05. Redis 集群架构

Posted IT BOY

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了05. Redis 集群架构相关的知识,希望对你有一定的参考价值。

目录

07 Redis集群架构

Pt1 Redis Master-Slave

Pt1.1 特性

Pt1.2 原理

Pt1.3 搭建

Pt1.4 缺点

Pt2 Redis Sentinel

Pt2.1 Sentinel原理

Pt2.2 Sentinel集群

Pt2.3 Sentinel缺点

Pt3 Redis Cluster

Pt3.2 代理层分片策略

Pt3.3 服务端分片策略

Pt3.4 节点通信


07 Redis集群架构

Redis的高性能可以助力业务的快速发展,但是随着架构和业务的不断演进,Redis也逐渐暴露出了一些问题。

  • 高性能。Redis本身性能很高,但是在一些高并发的场景下,单节点的性能仍然不够,需要更多的节点来分摊压力。

  • 高可用。在架构设计上,Redis的背后往往就是最核心的数据库存储,Redis分摊了数据库了访问请求,但是如果Redis发生宕机或者抖动,数据库会直接暴露在高并发的请求下,会导致数据库被击穿甚至导致服务层面的雪崩,带来灾难性的影响。

  • 可扩展。单节点的Redis,内存和CPU等硬件都有很大限制,往往很难横向扩展。

高性能、高可用和可扩展性,往往有两种解决方案,冗余和分片。

  • 冗余。给Redis节点增加一个或多个副本,副本有不同角色,当主节点发生故障,将某个从节点改为主节点,继续提供服务。

  • 分片。将数据拆分到多个节点分散存储。

有3中集群搭建方案,下面详细讲解。

  • Redis Master-Salve模式

  • Redis Sentinel模式

  • Redis Cluster模式


Pt1 Redis Master-Slave

Pt1.1 特性

主从复制是比较简单的集群,集群中的节点有主从之分,主节点负责数据写入和读取,从节点与主节点保持数据同步,并对外提供读服务。同时主从关系可以级联,从节点也可以有从节点。

主从架构的集群非常简单,就是单节点的升级版。

  • 提升可用性。单节点不稳定,通过主从多节点实现数据冗余和故障恢复,提升可用性。

  • 提升读性能。单节点读性能不够,通过主从多节点分担读压力。

但是主从架构的短板也非常明显。

  • 无法自动选主。主从架构没有实现自动选主功能,主节点宕机后需要手动切换到从节点,相对来说切换时效性就很难保证。

  • 写操作性能没有提升。虽然从节点可以分担读操作的压力,但是写操作只能在主节点上处理(单节点压力),如果主节点因为写操作压力大而崩溃,那么切换主节点后仍然很难抗住压力,所以提升可用性也只是理论的。

  • 无法横向扩容。随着业务请求量的不断增加,Redis集群性能不够用的时候,整个集群无法横向扩容,只能垂直升级节点性能。

所以主从架构适合简单,请求并发不是特别高的场景,业务增长不是特别快的场合,比如存储一些字典数据、分布式Session等就很合适。架构简单,又能满足业务需求,维护起来比较轻松。

Pt1.2 原理

(1) 主从连接

slave节点启动或者刚使用命令连上master时,会在本地保存master节点的信息,包括master的host和ip。在内部,slave节点会启动一个定时任务,每秒钟检查是否有新的master节点要连接和复制,如果发现有master节点,就建立连接,然后slave节点就专门建立一个专门处理复制工作的文件事件处理器负责后续内容复制工作。

同时,slave节点会定时给主节点发送ping命令,让master感知到slave节点的存活。

(2) 全量数据同步

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  • 从服务器连接主服务器,发送SYNC命令;

  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件,并使用缓冲区记录此后执行的所有写命令;

  • 主服务器BGSAVE执行完后,向从服务器发送快照文件,并在发送期间继续记录被执行的写命令;

  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;

  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;

  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

(3)增量数据同步

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

还有一种情况,slave节点网络异常,断开连接一段时间后再次连接上了master节点,这时候要怎么处理呢?清空数据然后重新做一次全量同步吗?当然不是,效率太低了。通过info replication不仅可以查看集群状态,也可以查看当前主从复制的偏移量,找到偏移量,就可以从偏移量之后开始继续复制。

redis 2.8版本以前,并不支持部分同步,当主从服务器之间的连接断掉之后,master服务器和slave服务器之间都是进行全量数据同步,但是从redis 2.8开始,即使主从连接中途断掉,也不需要进行全量同步,因为从这个版本开始融入了部分同步的概念。

部分同步的实现依赖于在master服务器内存中给每个slave服务器维护了一份同步日志和同步标识,每个slave服务器在跟master服务器进行同步时都会携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后,slave服务器隔断时间(默认1s)主动尝试和master服务器进行连接,如果从服务器携带的偏移量标识还在master服务器上的同步备份日志中,那么就从slave发送的偏移量开始继续上次的同步操作,如果slave发送的偏移量已经不再master的同步备份日志中(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内master服务器接收到大量的写操作),则必须进行一次全量更新。

在部分同步过程中,master会将本地记录的同步备份日志中记录的指令依次发送给slave服务器从而达到数据一致。

(4) 主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

主从同步有如下策略:

  • 1)Redis使用异步复制。但从Redis 2.8开始,从节点周期性的应答从复制流中处理的数据量。

  • 2)一个主节点可以有多个从节点。

  • 3)从节点也可以接受其他从节点的连接。除了多个从节点连接到一个主节点之外,多个从节点也可以连接到一个从节点上,形成一个图状结构。

  • 4)主从复制对于主redis节点来说是非阻塞的,这意味着当从节点在进行主从复制同步过程中,主redis仍然可以处理外界的访问请求;

  • 5)主从复制对于从redis节点来说也是非阻塞的,这意味着,即使从redis在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从redis返回的是以前老的数据,如果你不想这样,那么在启动redis时,可以在配置文件中进行设置,那么从redis在复制同步过程中来自外界的查询请求都会返回错误给客户端;(虽然说主从复制过程中对于从redis是非阻塞的,但是当从redis从主redis同步过来最新的数据后还需要将新数据加载到内存中,在加载到内存的过程中是阻塞的,在这段时间内的请求将会被阻,但是即使对于大数据集,加载到内存的时间也是比较多的);

  • 6)主从复制提高了redis服务的扩展性,避免单个redis节点的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案;

  • 7)使用主从复制可以为主节点免除把数据写入磁盘的消耗,主节点关闭数据持久化道磁盘操作,而通过连接让一个配置的从redis节点及时的将相关数据持久化到磁盘,不过这样会存在一个问题,就是主redis节点一旦重启,因为主redis节点数据为空,这时候通过主从同步可能导致从redis节点上的数据也被清空;

(5) 无盘复制

通常来讲,一个完全重新同步需要在磁盘上创建一个RDB文件,然后加载这个文件以便为从服务器发送数据。如果使用比较低速的磁盘,这种操作会给主服务器带来较大的压力。Redis从2.8.18版本开始尝试支持无磁盘的复制。使用这种设置时,子进程直接将RDB通过网络发送给从服务器,不使用磁盘作为中间存储。

无盘复制适用于主节点所在机器磁盘性能较差但网络带宽较宽裕的场景。

Pt1.3 搭建

我们搭建一主一从的Redis集群,看看用到哪些配置和命令。

(1)修改Redis配置文件

修改主从节点的redis.conf,分别修改一下配置。

 port 6380   # 主节点6380,从节点6381
 daemonize no
 protected-mode no
 dir ./
 appendonly yes
 pidfile /var/run/redis_6380.pid # 主节点6380,从节点6381

(2)启动Redis主从节点

通过不同端口启动Redis Docker镜像,来启动两个Redis独立服务。

 # 6380端口作为Leader节点
 [root@VM-0-17-centos conf]# docker run -p 6380:6380 --name leader-redis -v /root/app/leader-follower-redis/leader/conf/redis.conf:/etc/leader-redis/redis.conf -v /root/app/leader-follower-redis/leader/data/:/data -d redis:latest redis-server /etc/leader-redis/redis.conf --appendonly yes
 fbf4e23612e037c4830ff2c66b3664f21c5ea38d83ff2c71c87dd16c10edaa7b
 ​
 # 6381作为follower节点
 [root@VM-0-17-centos conf]# docker run -p 6381:6381 --name follower-redis -v /root/app/leader-follower-redis/follower/conf/redis.conf:/etc/follower-redis/redis.conf -v /root/app/leader-follower-redis/follower/data/:/data -d redis:latest redis-server /etc/follower-redis/redis.conf --appendonly yes
 42e208d9bd2d653233f459fe1f7177bd36babe7d904afaf8e94dd7cdf3bed3cb
 ​
 # 查看节点状态,这个时候还没组成主从复制的集群
 [root@VM-0-17-centos leader-follower-redis]# docker ps
 CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                              NAMES
 42e208d9bd2d   redis:latest   "docker-entrypoint.s…"   13 minutes ago   Up 13 minutes   6379/tcp, 0.0.0.0:6381->6381/tcp   follower-redis
 fbf4e23612e0   redis:latest   "docker-entrypoint.s…"   13 minutes ago   Up 13 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp   leader-redis
 ​
 # 尝试单独连接leader节点
 [root@VM-0-17-centos leader-follower-redis]# docker exec -it leader-redis redis-cli -p 6380
 127.0.0.1:6379> set name lucas
 OK
 127.0.0.1:6379> get name
 "lucas"
 ​
 # 尝试单独连接follower节点
 [root@VM-0-17-centos leader-follower-redis]# docker exec -it follower-redis redis-cli -p 6381
 127.0.0.1:6379> set name tracy
 OK
 127.0.0.1:6379> get name
 "tracy"
 ​
 # 再连上leader节点,可以看到数据并不是联通的
 [root@VM-0-17-centos leader-follower-redis]# docker exec -it leader-redis redis-cli -p 6380
 127.0.0.1:6379> get name
 "lucas"

(3)配置主从集群

方式一:通过启动命令配置主从集群

Redis从节点的启动命令中,指定leader节点,格式为:./redis-server --slaveof ip port

 # 修改从节点的redis.conf文件
 [root@VM-0-17-centos conf]# ll
 -rw-r--r-- 1 root root 84839 Dec 25 15:31 redis.conf
 [root@VM-0-17-centos conf]# pwd
 /root/app/leader-follower-redis/follower/conf
 .....
 ​
 # 在redis.conf下新增此列配置
 replicaof 172.17.0.17 6380

启动后,从节点会连接到主节点组成集群

方式二:通过命令改变运行中集群状态

节点启动后,可以在从节点中执行命令,来指定leader节点,格式为:slaveof ip port

 [root@VM-0-17-centos leader-follower-redis]# docker ps
 CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                              NAMES
 ce8ef1cea5b5   redis:latest   "docker-entrypoint.s…"   14 minutes ago   Up 14 minutes   6379/tcp, 0.0.0.0:6381->6381/tcp   follower-redis
 fcb38dc12e0b   redis:latest   "docker-entrypoint.s…"   14 minutes ago   Up 14 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp   leader-redis
 [root@VM-0-17-centos leader-follower-redis]# docker exec -it follower-redis redis-cli -p 6381
 127.0.0.1:6379> slaveof 172.17.0.17 6380
 OK

方式三:启动命令行配置主从关系

启动服务时,通过--slaveof ip port指定master节点。

 docker run -p 6381:6381 --name follower-redis -v /root/app/leader-follower-redis/follower/conf/redis.conf:/etc/follower-redis/redis.conf -v /root/app/leader-follower-redis/follower/data/:/data -d redis:latest redis-server /etc/follower-redis/redis.conf --appendonly yes --slaveof 172.17.0.17 6380

(4)查看集群状态

通过info replication命令可以查看集群状态。

 127.0.0.1:6380> info replication
 # Replication
 role:master             # 当前节点角色
 connected_slaves:1      # 从节点的个人
 slave0:ip=172.18.0.1,port=6381,state=online,offset=154,lag=0
 master_replid:dadc68598667c5f9d907414e486549ca85ba6e69
 master_replid2:0000000000000000000000000000000000000000
 master_repl_offset:154  # 主节点当前数据存储偏移量
 second_repl_offset:-1
 repl_backlog_active:1
 repl_backlog_size:1048576
 repl_backlog_first_byte_offset:1
 repl_backlog_histlen:154
 127.0.0.1:6380> 

(5)解除集群状态

从节点如果想摆脱主节点,通过命令[slaveof no one]来接触和主节点的绑定关系。

 # 从节点解除集群关系
 127.0.0.1:6381> slaveof no one
 OK
 ​
 # 查看集群状态
 127.0.0.1:6380> info replication
 # Replication
 role:master
 connected_slaves:0
 master_replid:dadc68598667c5f9d907414e486549ca85ba6e69
 master_replid2:0000000000000000000000000000000000000000
 master_repl_offset:2101
 second_repl_offset:-1
 repl_backlog_active:1
 repl_backlog_size:1048576
 repl_backlog_first_byte_offset:1
 repl_backlog_histlen:2101

Pt1.4 缺点

主从复制解决了读性能和扩展性的问题,但是没有解决组件的高可用和写性能问题。在一主一从或一注多从的情况下,如果leader节点宕机,集群就无法对外提供服务了,依然存在单点问题。

虽然可以手动切换集群主节点,但是每次都要手动切换,费时费力,服务总会暂停一段时间。所以Redis还提供了一个机制,在故障时实现自动选主的能力,就是Redis Sentinel。


Pt2 Redis Sentinel

Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案。实际上这意味着你可以使用Sentinel模式创建一个可以不用人为干预而应对各种故障的Redis部署。它的主要功能有以下几点:

  • 监控:Sentinel不断的检查master和slave是否正常的运行。

  • 通知:如果发现某个redis节点运行出现问题,可以通过API通知系统管理员和其他的应用程序。

  • 自动故障转移:能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。

  • 配置提供者:哨兵作为Redis客户端发现的权威来源:客户端连接到哨兵请求当前可靠的master的地址。如果发生故障,哨兵将报告新地址。

Pt2.1 Sentinel原理

主从复制并没有完全解决分布式高可用的问题,要实现高可用,需要实现以下两点:

  1. 主节点发生故障时,能够及时被检测到,并实现主节点自动切换;

  2. 主节点切换后,从节点能够感知到主节点的变更,自动获取最新的主节点信息并完成数据同步处理;

这就是Sentinel角色要做的处理,Redis Sentinel又叫做哨兵,它是独立运行的一种特殊模式的Redis服务,可以监听主从节点的状态。它不提供数据读写,主要负责保证集群的高可用。Sentinel需要做以下3件事:

  1. 监控Leader节点状态,能够及时检测到Leader节点发生故障;

  2. 选举新的Leader节点;

  3. 保证Sentinel自身高可用;

(1)Leader下线

Sentinel需要检测Leader节点的状态,以便能够及时感知到Leader节点的异常。

  • 主观下线:Sentinel默认以每秒1次的频率向Leader节点发送PING命令,如果在指定时间内(默认30秒)没有收到回复,则会将该Leader服务标记为下线。

  • 客观下线:但是,这也有可能是Sentinel节点网络问题导致通信异常,而不是Leader节点出现问题。所以此时Sentinel节点会询问集群中其它Sentinel节点(sentinel集群后面会说),确认Leader节点是否正常通信,如果多数Sentinel节点都认为Leader节点已下线,Leader节点才被真正确认下线。

  • 确认Leader节点故障下线后,要选出新的Leader节点,这也是需要Sentinel来处理。

(2)Leader选举

Sentinel根据以下四个规则选出合适的Follower节点作为新的Leader节点。

  1. 如果follower节点与Sentinel节点断开连接的时间比较久,超过了某个时间阈值,则直接失去了选举权;

  2. 如果多个节点拥有选举权,则查看配置文件中(replica-priority)优先级的设置,数据越小优先级越高;

  3. 如果优先级相同,则看谁从Leader节点同步的数据最多(复制偏移量最大),选择最多的节点;

  4. 如果数据同步也相同,则选择进程ID最小的。

当选举出新的Leader节点后,Sentinel会执行两个命令操作:

  1. Sentinel会向新的主节点发送slaveof no one命令,让它成为不附属于任何节点的独立节点;

  2. Sentinel会向其它节点发送slaveof ip port命令,让他们成为这个节点的从节点;

集群就可以继续对外提供服务。

Pt2.2 Sentinel集群

(1) Sentinel集群通信

前面介绍了Sentinel可以检测Leader节点的故障以及选举新的Leader节点,从而保持Redis集群高可用,但是如果Sentinel挂了,那不是没人做这个事情了,不就又歇菜了吗?如果保证Sentinel的高可靠呢?答案就是集群部署。

为了保证哨兵服务的可用性,需要对Sentinel做集群部署。不过和Redis主从节点不同,Sentinel节点之间都是对等的,没有主从之分,Sentinel节点既监控所有的Redis服务,Sentinel节点之间也互相监控。

Sentinel节点之间通过Redis的发布订阅功能进行互相通信。

  1. 新的Sentinel节点上线时,会给所有的Redis节点(Leader/Follower)中名为[sentinel:hello]的channel发送消息。

  2. 每个Sentinel节点都订阅了Redis节点(Leader/Follower)中名为[sentinel:hello]的channel,所以能互相感知到对方的存在,从而进行监控。

Sentinel节点通过info命令获取Redis主从节点的信息。

(2) Sentinel集群选举

Sentinel节点之间没有主从之分,通过发布订阅互相监控和通信。当Leader节点发生故障,我们需要选举新的Leader节点时和执行命令时,由哪个Sentinel节点来执行处理呢?总要有个节点代表整个Sentinel集群来完成这件事。所以在Sentinel集群内部也有一个Leader节点,这个节点通过自动选举产生,由Sentinel集群的Leader节点负责处理Redis集群的Leader选举。

Sentinel集群通过Raft算法实现Sentinel选举。参照[相关知识]中对于Raft算法的介绍。

但是Sentinel的Raft算法和通用Raft算法逻辑略有不同:

  1. 主从节点的Leader客观下线触发选举,而不是过了Sentinel集群的election timeout时间开始选举。

  2. Sentinel集群中,新Leader选举后,不会把自己成为Leader的消息发送给其它Sentinel节点。等待主从集群选出新的主节点后,其它sentinel节点会检测到新的主节点正常工作,于是会去掉自身标记的客户下线逻辑。

Pt2.3 Sentinel缺点

  1. 只有一个Leader节点,主从切换过程中会丢失数据;

  2. 单点写,无法解决水平扩容的问题;


Pt3 Redis Cluster

如果数据量非常大,单点的Redis能力就捉襟见肘了,这时候就需要进行数据分片,解决Redis水平扩容的问题。我们来看看Redis水平扩容的集群方案。

  • 客户端分片

  • 代理层分片

  • 服务端分片

Pt3.1 客户端分片策略

客户端分片是在客户端执行操作时,根据key计算对应的Redis实例,然后直接请求对应实例进行操作。比如常用的Jedis客户端就包含了对分片策略的实现。

分片的实现一般是依靠哈希运算取模。比如有N个Redis节点,hash(key)%N计算出key对应的Redis实例,直接连接对应的实例进行读写操作。方式比较简单,属于静态分片规则,如果N发生变更(新增或减少)将会导致对N取模的数据发生变化,所有实例中的数据都会重新分布,所以扩容或者故障的代价非常高昂。

(1) 一致性哈希

简单的哈希取模不好用,通常的用法是一致性哈希算法。

  1. 把所有的哈希值组织成一个虚拟的圆环,首(0)尾(2^32 - 1)重叠。

  2. 假如我们有4个Redis节点,首先根据机器名称或者IP计算出哈希值,然后分散到圆环中。

  3. 现在我们要对key进行操作,计算出key的哈希值,然后沿着哈希环顺时针找到第一个节点,就是数据存储的位置。

新增节点时,只需要将上一个节点到新增节点之间的哈希环上数据迁移到新增节点上即可。

删除某个节点时,将该节点上数据全部迁移到下一个节点。

(2) 虚拟节点

一致性哈希解决了节点动态增减时,所有数据都要重新分布的问题,他只会影响相邻节点的数据,对其他节点都没有影响。但是他有一个缺点:一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。正常情况项目中节点也不会很多,我们项目里3主3从的配置已经很能扛了。

比如刚才的4实例场景,可能50%甚至更多的数据分布在节点1上,如果节点1发生故障,影响的范围非常大,远远超出我们评估的影响范围,那么生产上将会造成难以评估的风险。

Redis通过引入虚拟节点解决数据分布不均匀的问题。虚拟节点其实跟后面Redis Cluster中哈希槽的方案思路是一样的。

为了避免出现数据倾斜问题,一致性Hash算法引入了虚拟节点的机制,也就是每个机器节点会进行多次哈希,最终每个机器节点在哈希环上会有多个虚拟节点存在,使用这种方式来大大削弱甚至避免数据倾斜问题。将虚拟节点与实际节点关联,这样每个真实节点就关联了很多的虚拟节点,能够在一定程度上保证数据随机分布的均匀。

比如Jedis中为每个Redis节点创建160个虚拟节点,通过红黑树来保存所有节点。存取键值时,计算key的哈希值,然后从红黑树上找到比哈希值大的最小节点上的JedisShareInfo。

 public class Sharded<R, S extends ShardInfo<R>> {
     public static final int DEFAULT_WEIGHT = 1;
     //用于保存服务器节点(包括虚拟节点)
     private TreeMap<Long, S> nodes;
     //哈希函数,这里使用MurmurHash算法
     private final Hashing algo;
     private final Map<ShardInfo<R>, R> resources;
     private Pattern tagPattern;
     public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\\\{(.+?)\\\\}");
     //省略非关键构造函数……
 ……
 public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
     this.resources = new LinkedHashMap();
     this.tagPattern = null;
     this.algo = algo;
     this.tagPattern = tagPattern;
     this.initialize(shards);
 }
 ​
 private void initialize(List<S> shards) {
     //底层使用TreeMap(红黑树)保存每个节点
     this.nodes = new TreeMap();
     //shards.size就是我们传入的真实的服务器数量,每个真实节点都有些和虚拟节点关联
     for(int i = 0; i != shards.size(); ++i) {
         ShardInfo shardInfo = (ShardInfo)shards.get(i);
         int n;
         if(shardInfo.getName() == null) {
         //默认一个真实节点关联160个虚拟节点(权值DEFAULT_WEIGHT默认为1)
             for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {
                 //使用哈希算法计算出散列值并放入treemap,根据默认的节点名称和虚拟节点个数等信息作为key
                 this.nodes.put(Long.valueOf(this.algo.hash("SHARD-" + i + "-NODE-" + n)), shardInfo);
             }
         } else {
             //使用哈希算法计算出散列值并放入treemap,根据传入的节点名称和虚拟节点个数等信息作为key
             for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {
                 this.nodes.put(Long.valueOf(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n)), shardInfo);
             }
         }
         this.resources.put(shardInfo, shardInfo.createResource());
     }
 ​
 }
 ​
 // 获取红黑树子集,找到比key哈希值大的最小节点
 public S getShardInfo(byte[] key) {
     //模拟沿环的顺时针找到一个虚拟节点,借助SortedMap的比较功能
     //tailMap方法返回键大于或等于传入key的Map
     SortedMap tail = this.nodes.tailMap(Long.valueOf(this.algo.hash(key)));
     //得到大于当前key的那个Map,然后从中取出第一个key,即大于且离它最近的那个key。返回该虚拟节点对应的真实机器节点的信息
     return tail.isEmpty()?(ShardInfo)this.nodes.get(this.nodes.firstKey()):(ShardInfo)tail.get(tail.firstKey());
 }

使用SharedJedis之类的客户端分片策略,相对简单,不依赖其它组件。但是不能动态伸缩,客户端需要维护分片策略。


Pt3.2 代理层分片策略

代理层分片是指,在多个Redis主从Group的基础上,创建一层代理层,由代理层处理哈希路由的问题。比如典型的解决方案有,Twitter的Twemproxy和国内豌豆荚开源的Codis。

以Codis为例,Codis把所有的key分成N个槽(例如,1024个),每个槽对应一个Redis分组(一个分组就是一个Redis主从实例)。Codis对key做CRC32(hash)运算得到32位数字,然后对N取模,找到对应的槽,然后分配到槽对应的Redis实例上。

Codis的槽位映射关系是保存在CodisProxy中的,所以proxy也存在单点问题,如果要解决proxy单点问题,需要做集群部署。多个proxy之间数据同步可以通过Zookeeper等组件来实现。

新增节点时,可以为节点指定特定的槽位,Codis也提供了自动均衡的策略。

Codis不支持事务。

Codis的方案就不详细介绍了,因为Redis官方提供了分布式解决方案,很多原理和Codis类似,我们重点看下Redis Cluster的解决方案。


Pt3.3 服务端分片策略

Redis Cluster是在Redis 3.0中被推出的,用来解决分布式问题。他是去中心化的,并且可以实现高可用,多个主节点是对等关系,都可以为客户端提供服务。

Redis Cluster要解决一下的问题:

  1. 数据均匀分片;

  2. 客户端能够访问到数据节点;

  3. 重新分片保证服务正常;

  4. 节点故障时能够快速恢复;

(1) 数据均匀分布

Redis Cluster使用虚拟槽来保证数据分布的均匀。

  1. Redis Cluster创建了16384个槽(slot),每个节点负责一定区间的slot。比如,只有两个节点,则node1负责[0, 8191],node2负责[8192, 16385];有10个节点,则将slot均匀分成10部分,以此类推。

  2. 数据分布到Redis节点是对key使用CRC16(hash)算法在对16384取模,得到slot的值,然后分配到对应位置;

  3. 每个Leader节点会维护自己负责的slot,用一个bit序列标识。比如第0位是1,则代表第一个slot是该节点负责;

所以,key和slot的关系是永远不变的,变化的只是Redis node和slot之间的关系。使用命令[cluster keyslot key]查看key属于的slot。

但是,有些相关的key我不希望他们跨节点存储,希望他们能够存储在同一个Redis节点中,怎么办呢。在key中加入[{hash tag}],Redis在计算槽编号时用{}的字符串进行计算,所以多个key可以落在同一个槽中。

 set name{qs} lucas
 set age{qs} 30

(2) 数据迁移

key和slot的关系是永远不变的,当集群中节点新增时,只需要把需要分配到此节点的slot数据迁移过来即可。

  1. 增加新的节点,此时新增节点没有哈希槽,不能分布数据;

  2. 在原来集群任意节点上执行reshard,可以完成数据的迁移;

  3. 也可以输入需要分配的slot数量和来源节点进行数据迁移。

(3) 请求重定向

客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot,如果在本地就在本地处理,否则返回moved给客户端,让客户端进行重定向。

 # 查看一个key对应的hash slot
 cluster keyslot mykey

用redis-cli的时候,可以加入-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令

(4) 高可用

Redis Cluster中保证集群高可用的思路和实现和Redis Sentinel是一样的。简单来说,针对A节点,某一个节点认为A宕机了,那么此时是主观宕机。而如果集群内超过半数的节点认为A挂了, 那么此时A就会被标记为客观宕机。

一旦节点A被标记为了客观宕机,集群就会开始执行故障转移。其余正常运行的master节点会进行投票选举(RAFT算法),从A节点的slave节点中选举出一个,将其切换成新的master对外提供服务。当某个slave获得了超过半数的master节点投票,就成功当选。

当选成功之后,新的master会执行slaveof no one来让自己停止复制A节点,使自己成为master。然后将A节点所负责处理的slot,全部转移给自己,然后就会向集群发PONG消息来广播自己的最新状态。

按照一致性哈希的思想,如果某个节点挂了,那么就会沿着那个圆环,按照顺时针的顺序找到遇到的第一个Redis实例。

而对于Redis Cluster,某个key它其实并不关心它最终要去到哪个节点,他只关心他最终落到哪个slot上,无论你节点怎么去迁移,最终还是只需要找到对应的slot,然后再找到slot关联的节点,最终就能够找到最终的Redis实例了。

(5) 优势

Redis Cluster特点:

  • 去中心化架构;

  • 数据按照slot分布在多个节点,节点间数据共享,可动态调整数据分布;

  • 可扩展性强,可以线性扩展到1000个节点,节点可动态添加和删除;

  • 高可用,主从切换自动failover;

Pt3.4 节点通信

Redis Cluster各个节点之间交换数据、通信所采用的一种协议,叫做gossip。gossip主要是为了解决在分布式数据库中,各个副本节点的数据同步问题。但随着技术的发展,gossip后续也被广泛运用于信息扩散、故障探测等等。Redis Cluster就是利用了gossip来实现自身的信息扩散的。

gossip消息类型

每个Redis节点每秒钟都会向其他的节点发送PING,然后被PING的节点会回一个PONG。Redis Cluster中,节点之间的消息类型有5种,分别是MEET、PING、PONG、FAIL和PUBLISH。

消息类型消息内容
MEET给某个节点发送MEET消息,请求接收消息的节点加入到集群中
PING每隔一秒钟,选择5个最久没有通信的节点,发送PING消息,检测对应的节点是否在线;同时还有一种策略是,如果某个节点的通信延迟大于了cluster-node-time的值的一半,就会立即给该节点发送PING消息,避免数据交换延迟过久
PONG当节点接收到MEET或者PING消息之后,会回一个PONG消息给发送方,代表自己收到了MEET或者PING消息。同时,节点也可以主动的通过PONG消息向集群中广播自己的信息,让其他节点获取到自己最新的属性,就像完成了故障转移之后新的master向集群发送PONG消息一样
FAIL用于广播自己的对某个节点的宕机判断,假设当前节点对A节点判断为宕机,就会立即向Redis Cluster广播自己对于A节点的判断,所有收到消息的节点就会对A节点做标记
PUBLISH用于向指定的Channel发送消息,某个节点收到PUBLISH消息之后会直接在集群内广播,这样一来,客户端无论连接到任何节点都能够订阅这个Channel

gossip优点

优点描述
扩展性网络可以允许节点的任意增加和减少,新增加的节点的状态最终会与其他节点一致。
容错性由于每个节点都持有一份完整元数据,所以任何节点宕机都不会影响gossip的运行
健壮性与容错性类似,由于所有节点都持有数据,地位平台,是一个去中心化的设计,任何节点都不会影响到服务的运行
最终一致性当有新的信息需要传递时,消息可以快速的发送到所有的节点,让所有的节点都拥有最新的数据

gossip缺点

gossip仍然存在一些缺点。例如消息可能最终会经过很多轮才能到达目标节点,而这可能会带来较大的延迟。同时由于节点会随机选出5个最久没有通信的节点,这可能会造成某一个节点同时收到n个重复的消息。

以上是关于05. Redis 集群架构的主要内容,如果未能解决你的问题,请参考以下文章

如何利用redis来进行分布式集群系统的限流设计

Redis集群高可用架构

Redis集群架构

ZOOkeeper,Eureka和Redis集群架构

ZOOkeeper,Eureka和Redis集群架构

ZOOkeeper,Eureka和Redis集群架构