事务是一个业务,也可以看成是一个逻辑工作单元,是为了保证业务的完整,数据的正确而推出的一种控制机制,原则上来讲,事务必须要满足ACID四个特性(原子性,一致性,隔离性,持久性),在多个事务
在并发执行,为更好保证事务的四个特性的实现,通常会对事务加锁,Redis为了性能,采用了乐观锁方式进行事务控制,它使用watch命令监视给定的key,当exec(提交事务)的时候,如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。注意watch的key是对整个连接有效的,如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。
redis进行事务控制时,通常是基于如下指令进行实现,例如:
Redis保证一个事务中的所有命令要么都执行,要么都不执行(原子性)。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。
例如:模拟转账,tony 500,jack 200,tony转给jack100。过程如下:
127.0.0.1:6379> set tony 500 OK 127.0.0.1:6379> set jack 200 OK 127.0.0.1:6379> mget tony jack 1) "500" 2) "200" 127.0.0.1:6379> multi #开启事务 OK 127.0.0.1:6379(TX)> decrby tony 100 #所有指令操作会进入到队列 QUEUED 127.0.0.1:6379(TX)> incrby jack 100 QUEUED 127.0.0.1:6379(TX)> mget tony jack QUEUED 127.0.0.1:6379(TX)> exec #提交事务 1) (integer) 400 2) (integer) 300 3) 1) "400" 2) "300" 127.0.0.1:6379> mget tony jack 1) "400" 2) "300" 127.0.0.1:6379>
注意redis事务太简单,没有回滚,而只有取消。
127.0.0.1:6379> mget tony jack 1) "400" 2) "300" 127.0.0.1:6379> multi OK 127.0.0.1:6379> incrby jack 100 QUEUED 127.0.0.1:6379> discard OK 127.0.0.1:6379> get jack "300" 127.0.0.1:6379> exec (error) ERR EXEC without MULTI
当出现错误指令时,事务也会自动取消。
127.0.0.1:6379> mget tony jack 1) "400" 2) "300" 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> incrby jack 100 QUEUED 127.0.0.1:6379(TX)> abcd (error) ERR unknown command `abcd`, with args beginning with: 127.0.0.1:6379(TX)> get jack QUEUED 127.0.0.1:6379(TX)> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get jack "300" 127.0.0.1:6379>
基于一个秒杀,抢购案例,演示redis乐观锁方式,例如
第一步:打开客户端1,执行如下操作
127.0.0.1:6379> set ticket 1 OK 127.0.0.1:6379> set money 0 OK 127.0.0.1:6379> watch ticket #乐观锁,对值进行观察,改变则事务失败 OK 127.0.0.1:6379> multi #开启事务 OK 127.0.0.1:6379> decr ticket QUEUED 127.0.0.1:6379> incrby money 100 QUEUED
第二步:打开客户端2,执行如下操作,演示还没等客户端1提交事务,此时客户端2把票买到了。
127.0.0.1:6379> get ticket "1" 127.0.0.1:6379> decr ticket (integer) 0
第三步,回到客户端1:提交事务,检查ticket的值
127.0.0.1:6379> exec (nil) #执行事务,失败 127.0.0.1:6379> get ticket “0” 127.0.0.1:6379> unwatch #取消监控
基于Jedis进行事务测试,代码如下:
package com.jt; import org.junit.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class JedisTransactionTests { @Test public void testTransaction(){ Jedis jedis=new Jedis("192.168.126.130",6379); jedis.auth("123456"); jedis.set("tony","300"); jedis.set("jack","500"); //实现操作,tony转账100给jack //开启事务 Transaction multi = jedis.multi(); //执行业务操作 try { multi.decrBy("tony", 100); multi.incrBy("jack", 100); int n=100/0;//模拟异常 //提交事务 multi.exec(); }catch(Exception e) { //出现异常取消事务 multi.discard(); } String tonyMoney=jedis.get("tony"); String jackMoney=jedis.get("jack"); System.out.println("tonyMoney="+tonyMoney); System.out.println("jackMoney="+jackMoney); jedis.close(); } }
package com.jt.demos; import redis.clients.jedis.Jedis; import redis.clients.jedis.Response; import redis.clients.jedis.Transaction; import java.util.List; /** * redis秒杀练习: * 模拟两个线程都去抢购同一张票(考虑乐关锁) */ public class SecondKillDemo02 { public static void secKill(){ Jedis jedis=new Jedis("192.168.126.130",6379); jedis.auth("123456"); jedis.watch("ticket","money"); String ticket = jedis.get("ticket"); if(ticket==null||Integer.valueOf(ticket)==0) throw new RuntimeException("已无库存"); Transaction multi = jedis.multi(); try { multi.decr("ticket"); multi.incrBy("money", 100); List<Object> exec = multi.exec(); System.out.println(exec); }catch (Exception e){ e.printStackTrace(); multi.discard(); }finally { jedis.unwatch(); jedis.close(); } } public static void main(String[] args) { Jedis jedis=new Jedis("192.168.126.130",6379); jedis.auth("123456"); jedis.set("ticket","1"); jedis.set("money","0"); Thread t1=new Thread(()->{ secKill(); }); Thread t2=new Thread(()->{ secKill(); }); t1.start(); t2.start(); } }
单个Redis支持的读写能力还是有限的,此时我们可以使用多个redis来提高redis的并发处理能力,这些redis如何协同,就需要有一定的架构设计,这里我们首先从主从(Master/Slave)架构进行分析和实现.
redis主从架构如图所示:
其中,master负责读写,并将数据同步到salve,从节点负责读操作.
基于redis,设计一主从架构,一个Master,两个Slave,其中Master负责Redis读写操作,并将数据同步到Slave,Slave只负责读.,其步骤如下:
第一步:删除所有原有的redis容器,例如:
docker rm -f redis容器名
第二步:进入你的宿主机docker目录,然后将redis01拷贝两份,例如:
cp -r redis01/ redis03
cp -r redis01/ redis03
第三步:启动三个新的redis容器,例如:
docker run -p 6379:6379 --name redis6379 \ -v /usr/local/docker/redis01/data:/data \ -v /usr/local/docker/redis01/conf/redis.conf:/etc/redis/redis.conf \ -d redis redis-server /etc/redis/redis.conf \ --appendonly yes
第四步 检测redis服务角色
启动三个客户端,分别登陆三台redis容器服务,通过info指令进行角色查看,默认新启动的三个redis服务角色都为master.
127.0.0.1:6379> info replication
\# Replication role:master connected_slaves:0 master_repl_offset:3860 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:2 repl_backlog_histlen:3859
第五步:检测redis6379的ip设置
docker inspect redis6379
…… "Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "c33071765cb48acb1efed6611615c767b04b98e6e298caa0dc845420e6112b73", "EndpointID": "4c77e3f458ea64b7fc45062c5b2b3481fa32005153b7afc211117d0f7603e154", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:02", "DriverOpts": null } }
第六步:设置Master/Slave架构
分别登陆redis6380/redis6381,然后执行如下语句
slaveof 172.17.0.2 6379
说明,假如master有密码,需要在slave的redis.conf配置文件中添加"masterauth 你的密码"这条语句,然后重启redis再执行slaveof 指令操作.
第七步:再次登陆redis6379,然后检测info
[root@centos7964 ~]# docker exec -it redis6379 redis-cli 127.0.0.1:6379> info replication
\# Replication role:master connected_slaves:2 slave0:ip=172.17.0.3,port=6379,state=online,offset=2004,lag=1 slave1:ip=172.17.0.4,port=6379,state=online,offset=2004,lag=1 master_failover_state:no-failover master_replid:5baf174fd40e97663998abf5d8e89a51f7458488 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:2004 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:2004
第八步: 登陆redis6379测试,master读写都可以
[root@centos7964 ~]# docker exec -it redis6379 redis-cli 127.0.0.1:6379> set role master6379 OK 127.0.0.1:6379> get role "master6379" 127.0.0.1:6379>
第九步: 登陆redis6380测试,slave只能读。
[root@centos7964 ~]# docker exec -it redis6380 redis-cli 127.0.0.1:6379> get role "master6379" 127.0.0.1:6379> set role slave6380 (error) READONLY You can't write against a read only replica. 127.0.0.1:6379>
Java中的读写测试分析,代码如下:
@SpringBootTest public class MasterSlaveTests { @Autowired private RedisTemplate redisTemplate; @Test void testMasterReadWrite(){//配置文件端口为6379 ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations.set("role", "master6379"); Object role = valueOperations.get("role"); System.out.println(role); } @Test void testSlaveRead(){//配置文件端口为6380 ValueOperations valueOperations = redisTemplate.opsForValue(); Object role = valueOperations.get("role"); System.out.println(role); } }
Redis的主从结构可以采用一主多从结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。
- Redis全量同步:
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
1)从服务器连接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
- Redis增量同步
Redis增量复制是指Slave初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
- 如果redis要支持10万+的的并发你会怎么做?
单机的redis几乎不太可能说QPS超过10万+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂,一般的单机也就在几万。真正实现redis的高并发,需要读写分离。对缓存而言,一般都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千。读的请求相对就会比较多,例如,一秒钟二十万次读。所以redis的高并发可以基于主从架构与读写分离机制进行实现。
- Redis的replication机制是怎样的?
(1)redis采用异步方式复制数据到slave节点。
(2)一个master node是可以配置多个slave node的。
(3)slave node做复制的时候,是不会block master node的正常工作的。
(4)slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了。
(5)slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量。
哨兵(Sentinel)是Redis的主从架构模式下,实现高可用性(high availability)的一种机制。
由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
第一步:打开三个redis客户端窗口,分别进入3台redis容器内部,在容器(Container)指定目录/etc/redis中执行如下语句:
cat <<EOF > /etc/redis/sentinel.conf sentinel monitor redis6379 172.17.0.2 6379 1 EOF
其中, 如上指令表示要的监控的master, redis6379为服务名, 172.17.0.2和6379为master的ip和端口,1表示多少个sentinel认为一个master失效时,master才算真正失效.
第二步:在每个redis容器内部的/etc/redis目录下执行如下指令,启动哨兵服务
redis-sentinel sentinel.conf
第三步:打开一个新的客户端连接窗口,关闭redis6379服务(这个服务是master服务)
docker stop redis6379
在其它客户端窗口,检测日志输出,例如
410:X 11 Jul 2021 09:54:27.383 # +switch-master redis6379 172.17.0.2 6379 172.17.0.4 6379 410:X 11 Jul 2021 09:54:27.383 * +slave slave 172.17.0.3:6379 172.17.0.3 6379 @ redis6379 172.17.0.4 6379 410:X 11 Jul 2021 09:54:27.383 * +slave slave 172.17.0.2:6379 172.17.0.2 6379 @ redis6379 172.17.0.4 6379
第四步:登陆ip为172.17.0.4对应的服务进行info检测,例如:
127.0.0.1:6379> info replication
\# Replication role:master connected_slaves:1 slave0:ip=172.17.0.3,port=6379,state=online,offset=222807,lag=0 master_failover_state:no-failover master_replid:3d63e8474dd7bcb282ff38027d4a78c413cede53 master_replid2:5baf174fd40e97663998abf5d8e89a51f7458488 master_repl_offset:222807 second_repl_offset:110197 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:29 repl_backlog_histlen:222779 127.0.0.1:6379>
从上面的信息输出发现,redis6381服务现在已经变为master。
对于sentinel.conf文件中的内容,我们还可以基于实际需求,进行增强配置,例如:
sentinel monitor redis6379 172.17.0.2 6379 1 daemonize yes #后台运行 logfile "/var/log/sentinel_log.log" #运行日志 sentinel down-after-milliseconds redis6379 30000 #默认30秒
其中:
例如: 基于cat指令创建sentinel.conf文件,并添加相关内容.
cat <<EOF > /etc/redis/sentinel.conf sentinel monitor redis6379 172.17.0.2 6379 1 daemonize yes logfile "/var/log/sentinel_log.log" sentinel down-after-milliseconds redis6379 30000 EOF
Redis单机模式可靠性保证不是很好,容易出现单点故障,同时其性能也受限于CPU的处理能力,实际开发中Redis必然是高可用的,所以单机模式并不是我们的终点,我们需要对目前redis的架构模式进行升级。
Sentinel模式做到了高可用,但是实质还是只有一个master在提供服务(读写分离的情况本质也是master在提供服务),当master节点所在的机器内存不足以支撑系统的数据时,就需要考虑集群了。
Redis集群架构实现了对redis的水平扩容,即启动N个redis节点,将整个数据分布存储在这N个redis节点中,每个节点存储总数据的1/N。redis集群通过分区提供一定程度的可用性,即使集群中有一部分节点失效或无法进行通讯,集群也可以继续处理命令请求。
对于redis集群(Cluster),一般最少设置为6个节点,3个master,3个slave,其简易架构如下:
第一步:准备网络环境
创建虚拟网卡,主要是用于redis-cluster能于外界进行网络通信,一般常用桥接模式。
docker network create redis-net
查看docker的网卡信息,可使用如下指令
docker network ls
查看docker网络详细信息,可使用命令
docker network inspect redis-net
第二步:准备redis配置模板
mkdir -p /usr/local/docker/redis-cluster
cd /usr/local/docker/redis-cluster
vim redis-cluster.tmpl
在redis-cluster.tmpl中输入以下内容
port ${PORT} cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 cluster-announce-ip 192.168.126.129 cluster-announce-port ${PORT} cluster-announce-bus-port 1${PORT} appendonly yes
其中:
各节点解释如下所示:
第三步:创建节点配置文件
在redis-cluser中执行以下命令
for port in $(seq 8010 8015); \ do \ mkdir -p ./${port}/conf \ && PORT=${port} envsubst < ./redis-cluster.tmpl > ./${port}/conf/redis.conf \ && mkdir -p ./${port}/data; \ done
其中:
[root@centos7964 ~]# for i in $(seq 1 5); > do echo $i; > done; 1 2 3 4 5 [root@centos7964 ~]#
cat /usr/local/docker/redis-cluster/801{0..5}/conf/redis.conf
第四步:创建集群中的redis节点容器
for port in $(seq 8010 8015); \ do \ docker run -it -d -p ${port}:${port} -p 1${port}:1${port} \ --privileged=true -v /usr/local/docker/redis-cluster/${port}/conf/redis.conf:/usr/local/etc/redis/redis.conf \ --privileged=true -v /usr/local/docker/redis-cluster/${port}/data:/data \ --restart always --name redis-${port} --net redis-net \ --sysctl net.core.somaxconn=1024 redis redis-server /usr/local/etc/redis/redis.conf; \ done
其中, --privileged=true表示让启动的容器用户具备真正root权限, --sysctl net.core.somaxconn=1024 这是一个linux的内核参数,用于设置请求队列大小,默认为128,后续启动redis的启动指令需要先放到这个请求队列中,然后依次启动.
创建成功以后,通过docker ps指令查看节点内容。
第五步:创建redis-cluster集群配置
docker exec -it redis-8010 bash
redis-cli --cluster create 192.168.126.129:8010 192.168.126.129:8011 192.168.126.129:8012 192.168.126.129:8013 192.168.126.129:8014 192.168.126.129:8015 --cluster-replicas 1
如上指令要尽量放在一行执行,其中最后的1表示主从比例,当出现选择提示信息时,输入yes即可。当集群创建好以后,可以通过一些相关指令查看集群信息,例如
cluster nodes #查看集群节点数 cluster info #查看集群基本信息
第六步:连接redis-cluster,并添加数据到redis
redis-cli -c -h 192.168.126.129 -p 8010
其中,这里-c表示集群(cluster),-h表示host(一般写ip地址),-p为端口(port)
其它:
在搭建过程,可能在出现问题后,需要停止或直接删除docker容器,可以使用以下参考命令:
批量停止docker 容器,例如:
docker ps -a | grep -i "redis-801*" | awk '{print $1}' | xargs docker stop
批量删除docker 容器,例如
docker ps -a | grep -i "redis-801*" | awk '{print $1}' | xargs docker rm -f
批量删除文件,目录等,例如:
rm -rf 801{0..5}/conf/redis.conf rm -rf 801{0..5}
以上就是基于docker搭建redis-cluster的简单步骤,实际应用中可能还要更复杂一些,该文仅用于参考。
@Test void testJedisCluster()throws Exception{ Set<HostAndPort> nodes = new HashSet<>(); nodes.add(new HostAndPort("192.168.126.129",8010)); nodes.add(new HostAndPort("192.168.126.129",8011)); nodes.add(new HostAndPort("192.168.126.129",8012)); nodes.add(new HostAndPort("192.168.126.129",8013)); nodes.add(new HostAndPort("192.168.126.129",8014)); nodes.add(new HostAndPort("192.168.126.129",8015)); JedisCluster jedisCluster = new JedisCluster(nodes); //使用jedisCluster操作redis jedisCluster.set("test", "cluster"); String str = jedisCluster.get("test"); System.out.println(str); //关闭连接池 jedisCluster.close(); }
第一步:配置application.yml,例如:
spring: redis: cluster: #redis 集群配置 nodes: 192.168.126.129:8010,192.168.126.129:8011,192.168.126.129:8012,192.168.126.129:8013,192.168.126.129:8014,192.168.126.129:8015 max-redirects: 3 #最大跳转次数 timeout: 5000 #超时时间 database: 0 jedis: #连接池 pool: max-idle: 8 max-wait: 0
第二步:编写单元测试类,代码如下:
package com.cy.redis; @SpringBootTest public class RedisClusterTests { @Autowired private RedisTemplate redisTemplate; @Test void testMasterReadWrite(){ //1.获取数据操作对象 ValueOperations valueOperations = redisTemplate.opsForValue(); //2.读写数据 valueOperations.set("city","beijing"); Object city=valueOperations.get("city"); System.out.println(city); } }
第一:Redis你要搞高并发的话,不可避免,要把底层的缓存搞得很好。例如,mysql的高并发,是通过一系列复杂的分库分表,订单系统,事务要求的,QPS到几万,比较高了。
第二:要做一些电商的商品详情页,真正的超高并发,QPS上十万,甚至是百万,一秒钟百万的请求量,只有redis是不够的,但是redis是整个大型的缓存架构中,支撑高并发的架构里面,非常重要的一个环节。
第三:你的底层的缓存中间件,缓存系统,必须能够支撑的起我们说的那种高并发,其次,再经过良好的整体的缓存架构的设计(多级缓存架构、热点缓存),支撑真正的上十万,甚至上百万的高并发。