Redis的集群模式:主从 & 哨兵 & 分片集群

  • 基于Redis集群解决单机Redis存在的问题,在之前学Redis一直都是单节点部署

单机或单节点Redis存在的四大问题:

  • 数据丢失问题:Redis是内存存储,服务重启可能会丢失数据  =>  利用Redis数据持久化的功能将数据写入磁盘
  • 并发能力问题:单节点Redis并发能力虽然不错,但也无法满足如618这样的高并发场景  =>  搭建一主多从集群,实现读写分离
  • 单点故障 - 故障恢复问题:如果Redis宕机,则服务不可用,需要一种自动的故障恢复手段  =>  利用Redis哨兵,实现健康检测和自动故障恢复
  • 存储能力问题:Redis基于内存存储,单节点能存储的数据量难以满足海量数据要求  =>  搭建分片集群,利用插槽机制实现动态扩容,从理论上来讲,它的存储能力是没有上限的

介绍一下Redis的集群模式?

  • Redis有三种主要的集群模式,用于在分布式环境中实现高可用性和数据复制,这些集群模式分别是:主从复制(Master-Slave Replication)、哨兵模式(Sentinel)和Redis Cluster模式。

1. Redis主从

  • 搭建主从架构
  • 主从数据同步原理

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。

主从模式简介

  • 主从复制是Redis最简单的集群模式,这个模式主要是为了解决单点故障的问题,所以将数据复制到多个副本中,这样即使有一台服务器出现故障,其它服务器依然可以继续提供服务。
  • 主从模式中,包括一个主节点(Master)和一个或多个从节点(Slave),主节点负责处理所有写操作和读操作,而从节点则复制主节点的数据,并且只能处理读操作,当主节点发送故障时,可以将一个从节点升级为主节点,实现故障转移(需要手动实现)。

1.1.主从集群结构

  • Redis的集群往往都是主从集群,它往往会有一个Master主节点,多个Slave / Replica从节点。 

下图就是一个简单的Redis主从集群结构:

如图所示,集群中有一个Master主节点、两个Slave从节点(现在叫Replica) =>  起码要包含三个节点,要有三个Redis实例,一主两从

  • 在Redis 5.0以前,从节点是叫Slave的,后来改名叫Replica  =>  都是代表从节点 

当我们通过Redis的Java客户端访问主从集群时,应该做好路由:

  • 如果是写操作,应该访问Master主节点,Master主节点会自动将数据同步给两个Slave从节点

  • 如果是读操作,建议访问各个Slave从节点,从而分担并发压力

Master主节点可以执行set命令(写操作),Replica从节点只能执行get命令(读操作) 。

为什么Redis要做成这种主从的集群,而不是传统的负载均衡集群呢?

  • 这是因为Redis应用当中大多数都是读多写少的场景,也就是查询比较多,而增删改比较少,既然如此,我们更多要应对的是读的压力,那我做了主从以后,我们还可以去做读写分离, 也就是说,我在执行写操作时,我让它去访问Master主节点,但如果执行的是读操作,那我就把你的请求分发到各个Slave或Replica从节点,这样我们一主多从,多个从节点共同承担读的请求,我们的读并发能力就可以得到一个比较大的提升,所以这就是为什么要搭建主从集群的一个原因了。
  • 但是做主从集群,必须保证一点,就是客户端在读取的时候,不管访问到哪个Slave从节点,都必须要保证拿到相同的结果     =>    如何保证?  需要让Master主节点把它上面的数据同步给每一个Slave从节点,这就是Redis主从架构的一个基本模式了

1.2 搭建主从集群 

1. 准备实例和配置  
  • 我们会在同一台虚拟机中开启3个Redis实例,模拟主从集群。  
  • 我们会在同一个虚拟机中利用3个Docker容器来搭建主从集群。

  • 要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。

  • 在同一个机器下还要修改每个实例的端口

2. 启动 & 开启主从关系

分别启动多个Redis实例虽然我们启动了3个Redis实例,但是它们并没有形成主从关系,我们需要通过命令来配置主从关系:

# Redis5.0以前
slaveof <masterip> <masterport>
# Redis5.0以后
replicaof <masterip> <masterport>
有临时和永久两种模式:
  • 永久生效:在redis.conf文件中利用slaveof命令指定Master主节点的IP和端口

  • 临时生效:直接利用redis-cli控制台输入slaveof命令,并且指定Master主节点的IP和端口

INFO replication:查看集群的状态信息

这样,就可以实现读写分离了,如果在Slave从节点上执行set写操作,会报错:

假设有A、B两个Redis实例,如何让B作为A的Slave从节点?

  • 在B节点执行命令:slaveof      A的IP     A的Port端口 

1.3 数据同步原理 

  • Redis主从同步的底层工作原理 

1. 全量同步

  • 主从第一次建立连接时,会执行全量同步  =>  主从第一次同步是全量同步,将Master主节点的所有数据都拷贝给Replica从节点,流程:

  • 将来基于数据版本可以做一个控制

首先,从节点通过replicaof命令与主节点建立连接,请求数据同步,主节点会判断是否为第一次连接,如果是,则将完整数据发送给Slave从节点,Master主节点使用RDB持久化技术,将内存中的数据生成RDB文件,并在后台发送给Slave从节点,Slave清空本地数据,加载Master主节点的RDB,这样就能确保Slave节点与Master节点的数据基本一致了。 

为什么是基本一致而不是完全一致呢? 
  • bgsave是异步执行的,在执行的过程当中,Matser节点的主进程还会去处理用户的请求,也就是说会有新的数据写入,新写入的数据并没有发给Slave从节点,所以Master节点的主进程除了处理这些新的数据以外,它还会把这些新写入的数据命令记录到repl_baklog的这样一个缓冲区当中,它是一个内存的缓冲区,repl_backlog里面记录的就是RDB期间收到的一些新的命令,并持续将log中的命令发送给Slave,repl_baklog里面记录的所有命令 + RDB文件里面的数据合在一起就是我们Master节点上的完整数据了。 

.........Slave执行接收到的命令,保持与Master之间的同步,这样就能保证Slave节点与Master节点上面的数据完全一致了。 

为什么叫全量同步呢?
  • 因为它有一个RDB的过程,它会把内存形成快照,整体发送给Slave,所以叫全量同步,全量同步是比较消耗性能的,因为生成RDB文件的速度比较慢,这种同步只有在第一次建立连接时才会去做。 
这里有一个问题,Master主节点如何得到Slave从节点是否是第一次来同步呢?Master如何判断Slave节点是不是第一次来做数据同步?  
  • 这里会用到两个很重要的概念作为判断依据:
  1. Replication Id 简称replid,是数据集的标记,replid一致则说明是同一数据集;每一个Master都有自己唯一的replid,Slave节点则会继承Master节点的replid。
  2. offset偏移量,记录已同步的数据量,它会随着记录在repl_baklog中的数据增多而逐渐增大;Slave完成同步时也会记录当前同步的offset,Slave的offset一定是小于等于Master的offset,如果Slave的offset小于Matser的offset,则说明Slave数据落后于Master,需要更新。

因此,Slave做数据同步,必须向Master声明自己的replication idoffset,Master才可以判断到底需要同步哪些数据,基于offset去判断数据同步的进度。 

由于我们在执行slaveof命令之前,所有Redis节点都是Master,即每个Slave在成为Slave之前,都有自己的replid和offset,当我们第一次执行slaveof命令,与Master建立主从关系时,发送的replid和offset是自己的,与Master肯定不一致,在数据同步请求时,Master判断Slave发送来的replid与自己的ID是否一致,如果发现不一致,则说明是第一次同步,说明这是一个全新的Slave,此时就知道要做全量同步了,主节点Master会将自己的replid和offset都发送给这个Slave,Slave保存这些信息到本地,并且Slave将Master的replid作为自己的ID,自此以后Slave的replid就与Matser一致了。

因此,Master判断一个节点是否是第一次同步的依据,就是看replid是否一致,流程如图:

全量同步的完整流程描述:

  • Slave从节点执行replicaof命令与Master主节点建立连接,并尝试请求增量同步
  • Master主节点判断replid,发现不一致,拒绝增量同步,开始进行全量同步 => Full resync
  • Master将完整内存数据生成RDB,发送RDB文件到Slave
  • Slave清空本地数据(Flushing old data  =>  清空本地旧数据),加载接收到的Master所发送的RDB文件
  • Master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给Slave
  • Slave执行接收到的命令,保持与Master之间的同步

2. 增量同步 

  • 主从第一次同步是全量同步,但如果Slave重启后同步(不管是因为故障重启还是自己重启),则执行增量同步 
  • 全量同步需要先做RDB,然后将RDB文件通过网络传输给Slave,成本太高了,因此除了第一次做全量同步,其它大多数时候Slave与Master都是做增量同步
什么是增量同步?
  • 就是只更新Slave与Master存在差异的部分数据  =>  差异的这一部分数据就是Slave在宕机期间错过的那些数据,增量同步是在主从服务器数据存在差异时进行的一种同步方式。 

增量同步是在Slave从服务器或者Slave从节点已经进行过全量同步后,Master主节点和Slave从节点之间的数据存在差异时进行的同步操作。 

  • Master主服务器从上次Slave同步的offset位置开始发送增量数据给Slave从服务器。 
那么Master怎么知道Slave与自己的数据差异在哪里呢?  =>  repl_baklog原理
  • 这就要说到全量同步时的repl-baklog文件了,这个文件的本质是一个固定大小的数组,只不过数组是环形,也就是说角标达到数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。 

repl_baklog中记录的就是Master当前的offset和Slave已经拷贝到的offset:

要增量同步拷贝的就是Slave的offset到Master最新的offset之间的这一部分数据所以repl_baklog的本质就是Slave与Master它们数据差异的一个缓冲区,repl_baklog只要记录了Slave尚未同步的这部分数据就ok了, 只要Slave与Master之间的数据差距别超过这个环的存储上限,那你永远能够从这个环里找到你所需要的数据,这样就永远能实现增量同步,但如果你Slave与Master之间的差距太多,已经超过了这个存储上限了,那么这个时候就没有办法做增量同步了。

也就是说,如果Slave出现网络阻塞,导致Master的offset远远超过了Slave的offset,如果Master持续写入新的数据,Master的offset就会覆盖repl_baklog中旧的数据,直到将Slave现在的offset也覆盖,此时如果Slave恢复,需要同步,却发现自己的offset都没有了,即Slave的offeset被覆盖,无法找到对应的offset,也就无法完成增量同步了,则此时只能做全量同步:


棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。
结论:
  • repl_baklog大小有上限,写满后会覆盖最早的数据,如果Slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于repl_baklog做增量同步,只能再次全量同步   =>  只能尽可能的避免。

3. 主从同步优化

  • 主从同步可以保证主从数据的一致性,非常重要。 
可以从以下几个方面来优化Redis主从集群:

一方面是尽可能的去减少全量同步,因为全量同步它的性能比较差,另一方面要去优化这个全量同步的性能 

  • 在Master中配置repl-diskless-sync  yes启用无磁盘复制(就是当我去写RDB文件时,我不把它写到磁盘的IO流了,而是写到网络当中直接发给Slave,减少了一次磁盘读写,性能就提高了很多),避免全量同步时的磁盘IO    =>   使用场景:你的磁盘比较慢,而网络却非常的快,如果你网络带宽不够,此时反而可能会导致网络阻塞
  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO以及网络IO
  • 适当提高repl_baklog的大小,发现Slave宕机时尽快实现故障恢复,尽可能避免全量同步
  • 限制一个Master上的Slave节点数量,如果实在是太多Slave,则可以采用主-从-从链式结构,减少Master压力  =>  让两个Slave从节点也去 

主-从-从链式结构是一种用于减轻主节点同步压力的方法。在这种结构中,一个主节点为主,两个从节点为从,再由这两个从节点为主,各自再带领两个从节点,这样,主节点可以将同步任务交给这两个中间的从节点,从而减少自己的压力。

主-从-从架构图

简述全量同步和增量同步的区别?

  • 全量同步:Master将完整内存数据生成RDB,发送RDB到Slave,后续命令则记录在repl_baklog,逐个发送给Slave
  • 增量同步:Slave提交自己的offset到Master,Master获取repl_baklog中从offset之后的命令给Slave 

什么时候执行全量同步?

  • Slave节点第一次连接Master节点时
  • Slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

  • Slave节点断开又恢复,并且在repl_baklog中能找到offset时

主从复制的优缺点总结:

  • 主从复制的优势在于简单易于,适用于读多写少的场景,它提供了数据备份功能,并且有很好的扩展性,只要增加更多的从节点,就能让整个集群的读的能力不断提升。
  • 但是主从模式最大的缺点,就是不具备故障自动转移的能力,没有办法做容错和恢复
  • 主节点和从节点的宕机都会导致客户端部分读写请求失败,需要人工介入让节点恢复或者手动切换一台从节点服务器变成主节点服务器才可以,并且在主节点宕机时,如果数据没有及时复制到从节点,也会导致数据的不一致。 

为了解决主从模式的无法自动容错及恢复的问题,Redis引入了一种哨兵模式的集群架构,从而来保证主从集群的高可用。

2. Redis哨兵 

哨兵模式简介

  • 哨兵模式是在主从复制的基础上加入了哨兵节点,哨兵节点是一种特殊的Redis节点,用于监控主节点和从节点的状态,当主节点发生故障时,哨兵节点可以自动进行故障转移,选择一个合适的从节点升级为主节点,并通知其它的从节点和应用程序进行更新。

​​​​​​​在原来的架构中,引入哨兵节点,其作用是监控Redis主节点和从节点的状态,每个Redis实例都可以作为哨兵节点,通常需要部署多个哨兵节点(Sentinel也是需要集群部署),以确保故障转移的可靠性。  

哨兵工作机制

  • Redis提供了哨兵(Sentinel)机制来监控主从集群的健康状态,实现主从集群的自动故障恢复,确保集群的高可用性。

哨兵的作用:

哨兵集群作用原理图
Sentinel哨兵的作用如下:
  • 集群状态监控:Sentinel会不断检查Master和Slave是否预期工作
  • 自动故障恢复或故障转移(failover - 故障转移或容错):如果Master故障,Sentinel会将一个Slave提升为Master,当故障实例恢复后会成为Slave,会以新的Master为主
  • 状态通知:Sentinel哨兵节点充当Redis客户端的服务发现来源,哨兵节点通过发布订阅功能来通过客户端有关主节点状态变化的消息,当集群发送failover故障转移时,Sentinel会将最新集群消息推送给Redis的客户端,客户端收到消息后,会更新配置,将新的主节点信息应用于连接池,从而使客户端可以继续与新的主节点进行交互 

Sentinel怎么知道一个Redis节点是否宕机呢? Sentinel如何判断一个redis实例是否健康?

服务状态监控

Sentinel哨兵节点基于心跳机制监测服务状态,哨兵节点每隔1秒向集群中的每个实例(所有主节点和从节点)发送ping命令,并通过实例的响应结果来做出判断:

  • 主观下线(sdown):如果某Sentinel哨兵节点发现某Redis实例未在规定时间内发送PONG响应,哨兵节点会将该节点标记未主观下线(我个人认为你下线了,但不一定真的下线了,因为是超时未响应,有可能是因为网络阻塞导致的响应超时)。
  • 客观下线(odown):若超过指定数量(quorum)的Sentinel哨兵节点都认为该Redis节点或该实例主观下线,则该实例客观下线  /  如果一个Redis节点被多数哨兵节点都标记为主观下线,那么它将被标记为客观下线。quorum可以在redis.config配置文件里面配,quorum值最好超过Sentinel实例数量的一半,Sentinel节点数量至少3台(3 / 2 => 2)

  • 当主节点被标记为客观下线时, 哨兵节点会触发故障转移过程,它会从所有健康的从节点中选举一个新的主节点,并将所有从节点切换到新的主节点,实现自动故障转移,同时,哨兵节点会更新所有客户端的配置,指向新的主节点。  

选举新的Master

一旦发现Master故障,Sentinel需要在Slave中选择一个作为新的Master,选择依据是这样的:

  • 首先判断Slave节点与Master节点断开时间长短,如果断开时间超过(down -after-milliseconds * 10)则会排除该Slave节点,因为断开时间越长,丢失的数据肯定就越多,这样的节点会直接排除,它就不具备选举权
  • 然后判断Slave节点的slave-priority值,默认值都为1,越小优先级越高,如果是0则永不参加选举  =>  既然默认都是1,跳过  
  • 如果slave-priority一样,则判断Slave节点的offset值,offset代表当前Slave节点与Master节点之间数据同步的一个进度,值越大说明跟Master之间的数据越接近,越大说明数据越新,优先级越高
  • 最后是判断Slave节点的run_id  -  运行ID大小,越小优先级越高(通过info  server可以查看run_id)
问题来了,当选出一个新的master后,该如何实现身份切换呢?故障转移步骤有哪些? 
  • 首先要在多个Sentinel中选举一个leader  =>  第一个确认Master客观下线的人会立刻发起投票,一定会成为leader
  • 由leader执行failover故障转移
  • 选定一个Slave作为新的Master,并且执行slaveof  no  one命令,让该节点成为Master
  • 然后让所有其它Slave节点都执行   slaveof  新Master的IP  新Master的Port端口  命令,让这些Slave成为新Master的从节点,开始从新的Master上同步数据
  • 最后,Sentinel修改故障节点的配置,将故障节点标记为Slave,当故障节点恢复后会自动成为新的Master的Slave节点

RedisTemplate的哨兵模式

  • 在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发送变化,Redis的客户端必须感知这种变化,及时更新连接信息。
  • Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换(RedisTemplate底底层就用的lettuce)。 

可以利用RedisTemplate连接哨兵集群:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
总结:
  • Redis哨兵这个集群模式的优点就是为整个集群系统提供了一种故障转移和恢复的能力。

3. Redis分片集群