Redis与分布式


主从复制

avatar

基本概念

有多个redis服务器,只有一个主节点(Master),其余均为从节点(Slave),主节点可以进行读写操作,从节点只能进行读操作.数据的复制是单向的,从节点中的内容是由主节点中复制过来的(直接复制/间接复制).

一个主节点可以有多个从节点(也可以没有),一个从节点只能有一个主节点.redis主从复制是异步复制,主节点收到写命令,先写到内部缓冲区,然后异步发送给从节点.

配置

在配置文件中添加从节点信息:

1
slaveof 主节点IP 主节点端口

作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

Redis为什么需要主从复制

使用Redis主从复制的原因主要是单台Redis节点存在以下的局限性:

  • Redis虽然读写的速度都很快,单节点的Redis能够支撑每秒请求数(QPS)大概在5w左右,如果上千万的用户访问,Redis就承载不了,成为了高并发的瓶颈。

  • 单节点的Redis不能保证高可用,当Redis因为某些原因意外宕机时,会导致缓存不可用

  • CPU的利用率上,单台Redis实例只能利用单个核心,这单个核心在面临海量数据的存取和管理工作时压力会非常大。

主从复制流程

第一次同步一般是全量同步,后续基于已经建立的TCP长连接进行增量同步,如果TCP长连接断开,再次连接后进行同步,主节点会判断从节点要读的数据是否在repl_backlog_buffer缓冲区中,如果在,则进行增量同步,否则进行全量同步.

  1. 若启动一个Slave机器进程,则它会向Master机器发送一个“sync command”命令,请求同步连接。

  2. 无论是第一次连接还是重新连接,Master机器都会启动一个后台进程,将数据快照保存到数据文件中(执行rdb操作),同时Master还会记录修改数据的所有命令并缓存在数据文件中。

  3. 后台进程完成缓存操作之后,Maste机器就会向Slave机器发送数据文件,Slave端机器将数据文件保存到硬盘上,然后将其加载到内存中,接着Master机器就会将修改数据的所有操作一并发送给Slave端机器。若Slave出现故障导致宕机,则恢复正常后会自动重新连接。

  4. Master机器收到Slave端机器的连接后,将其完整的数据文件发送给Slave端机器,如果Mater同时收到多个Slave发来的同步请求,则Master会在后台启动一个进程以保存数据文件,然后将其发送给所有的Slave端机器,确保所有的Slave端机器都正常。

主从数据不一致

原因: 主从节点的命令复制是异步执行的.在主从节点命令传播阶段,主节点收到新的写命令后,会发送给从节点。但是,主节点并不会等到从节点实际执行完命令后,再把结果返回给客户端,而是主节点自己在本地执行完命令后,就会向客户端返回结果了。如果从节点还没有执行主节点同步过来的命令,主从节点间的数据就不一致了.

如何应对:
第一种方法,尽量保证主从节点间的网络连接状况良好,避免主从节点在不同的机房。

第二种方法,可以开发一个外部程序来监控主从节点间的复制进度。具体做法:

  • Redis 的 INFO replication 命令可以查看主节点接收写命令的进度信息(master_repl_offset)和从节点复制写命令的进度信息(slave_repl_offset),所以,我们就可以开发一个监控程序,先用 INFO replication 命令查到主、从节点的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset,这样就能得到从节点和主节点间的复制进度差值了。

  • 如果某个从节点的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从节点连接进行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有从节点都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。

主从切换如何减少数据丢失?

主从切换过程中,产生数据丢失的情况有两种:

  • 异步复制同步丢失
  • 集群产生脑裂数据丢失
    我们不可能保证数据完全不丢失,只能做到使得尽量少的数据丢失。

异步复制同步丢失
当客户端发送写请求给主节点的时候,客户端会返回 ok,接着主节点将写请求异步同步给各个从节点,但是如果此时主节点还没来得及同步给从节点时发生了断电,那么主节点内存中的数据会丢失。

Redis 配置里有一个参数 min-slaves-max-lag,表示一旦所有的从节点数据复制和同步的延迟都超过了 min-slaves-max-lag 定义的值,那么主节点就会拒绝接收任何请求。

假设将 min-slaves-max-lag 配置为 10s 后,根据目前 master->slave 的复制速度,如果数据同步完成所需要时间超过10s,就会认为 master 未来宕机后损失的数据会很多,master 就拒绝写入新请求。这样就能将 master 和 slave 数据差控制在10s内,即使 master 宕机也只是这未复制的 10s 数据。

那么对于客户端,当客户端发现 master 不可写后,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间(等 master 恢复正常)后重新写入 master 来保证数据不丢失,也可以将数据写入 kafka 消息队列,等 master 恢复正常,再隔一段时间去消费 kafka 中的数据,让将数据重新写入 master 。

集群产生脑裂数据丢失
如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。

这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在从节点中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。

这时候网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。

方案:当主节点发现「从节点下线的数量太多」,或者「网络延迟太大」的时候,那么主节点会禁止写操作,直接把错误返回给客户端。

在 Redis 的配置文件中有两个参数我们可以设置:

min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果主从同步的延迟超过 x 秒,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。

这两个配置项组合后的要求是,主节点连接的从节点中至少有 N 个从节点,「并且」主节点进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主节点就不会再接收客户端的写请求了。

即使原主节点是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从节点进行同步,自然也就无法和从节点进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主节点就会被限制接收客户端写请求,客户端也就不能在原主节点中写入新数据了。

等到新主节点上线时,就只有新主节点能接收和处理客户端请求,此时,新写的数据会被直接写到新主节点中。而原主节点会被哨兵降为从节点,即使它的数据被清空了,也不会有新数据丢失。我再来给你举个例子。

假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主节点因为某些原因卡住了 15s,导致哨兵判断主节点客观下线,开始进行主从切换。同时,因为原主节点卡住了 15s,没有一个从节点能和原主节点在 12s 内进行数据复制,原主节点也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主节点能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。

其他

过期key如何处理?
主节点中的key被删除或淘汰,会模拟一条删除命令发送给从节点,从节点收到这条命令后,会删除对应的key.

为了避免网络恢复时,频繁进行全量同步,该怎么做?
要将repl_backlog_buffer缓冲区调得大一些,至少为主节点一秒写的数据量重连时间2.

如何分摊主服务器的压力?
将一部分从节点升级为其他从节点的主节点

怎么判断 Redis 某个节点是否正常工作?
Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。

哨兵模式

经过之前的学习,我们发现,实际上最关键的还是主节点,因为一旦主节点出现问题,那么整个主从系统将无法写入,因此,我们得想一个办法,处理一下主节点故障的情况。实际上我们可以参考Spring的服务治理模式,比如Nacos和Eureka,所有的服务都会被实时监控,那么只要出现问题,肯定是可以及时发现的,并且能够采取响应的补救措施,这就是我们即将介绍的哨兵:

avatar

哨兵 (也是一个Redis服务器)会对所有的节点进行监控,如果发现主节点出现问题,那么会立即让从节点进行投票,选举一个新的主节点出来,这样就不会由于主节点的故障导致整个系统不可写(注意要实现这样的功能最小的系统必须是一主一从,再小的话就没有意义了)

那么怎么启动一个哨兵呢?我们只需要稍微修改一下配置文件(conf文件)即可,修改:

1
sentinel monitor name ip port number

其他配置都是次要的,我们都采用默认配置.

其中第一个和第二个是固定,第三个是为监控对象名称,随意,后面就是主节点的相关信息,包括IP地址和端口,最后的一个数字的意思是当有number个哨兵判断主节点挂掉后,我们认为主节点真的挂掉了,一般number是要比哨兵数量少的,因为哨兵也可能挂掉.如果number等于哨兵数量,如果哨兵挂掉了一个,那么就永远判断不出主节点是否挂了.number一般设置为哨兵数量/2+1.

不需要填写其他哨兵信息,如何组成哨兵集群?
哨兵节点之间通过redis的发布者/订阅者机制互相发现,主节点有一个sentinel:hello频道,哨兵可以将自己的ip和端口信息发到这个频道上,这样其他哨兵就可以通过订阅这个频道来获取到这个哨兵的信息,这样哨兵之间就可以建立网络,哨兵集群就形成了.

avatar

哨兵模式启动后,会自动监控主节点,然后还会显示那些节点是作为从节点存在的。如果主节点挂了,一个哨兵认为其主观下线,会向其他哨兵发起投票,如果认为主节点主观下线的数量到达设置得数,则认为主节点客观下线.之后就要进行主从切换了,哨兵节点中投票选出执行的leader.候选者为判断主节点客观下线的哨兵,候选者可以给自己投票,其他哨兵不能给自己投票,每个哨兵只有一次投票机会.任何候选者想要成为leader需要满足两个条件:

  • 拿到半数以上的票
  • 票数大于设置的number值

之后leader会选出新的主节点,将从节点指向新主节点,通知客户端主节点更换,将旧主节点变为从节点

那么,这个选新主节点的规则是怎样的呢?是在所有的从节点中随机选取还是遵循某种规则呢?

  1. 首先会根据优先级进行选择,可以在配置文件中进行配置,添加replica-priority配置项(默认是100),越小表示优先级越高。

  2. 如果优先级一样,那就选择偏移量最大的

  3. 要是还选不出来,那就选择runid(启动时随机生成的)最小的。

集群模式

如果我们服务器的内存不够用了,但是现在我们的Redis又需要继续存储内容,那么这个时候就可以利用集群来实现扩容。

因为单机的内存容量最大就那么多,已经没办法再继续扩展了,但是现在又需要存储更多的内容,这时我们就可以让N台机器上的Redis来分别存储各个部分的数据(每个Redis可以存储1/N的数据量),这样就实现了容量的横向扩展。同时每台Redis还可以配一个从节点,这样就可以更好地保证数据的安全性。

avatar

集群的作用

  • 数据分区:数据分区(或称数据分片)是集群最核心的功能。

    • 集群将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。
    • Redis单机内存大小受限问题,在介绍主从复制时有提及;例如,如果单机内存太大,bgsave和bgrewriteaof的fork操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出。
  • 高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似);当任一节点发生故障时,集群仍然可以对外提供服务。

那么问题来,现在用户来了一个写入的请求,数据该写到哪个节点上呢?我们来研究一下集群的机制

首先,一个Redis集群包含16384个插槽,集群中的每个Redis 实例负责维护一部分插槽以及插槽所映射的键值数据,那么这个插槽是什么意思呢?

实际上,插槽就是键的Hash计算后的一个结果,注意这里出现了计算机网络中的CRC循环冗余校验,这里采用CRC16,能得到16个bit位的数据,也就是说算出来之后结果是0-65535之间,再进行取模,得到最终结果:

Redis key的路由计算公式

结果的值是多少,就应该存放到对应维护的Redis下,比如Redis节点1负责0-25565的插槽,而这时客户端插入了一个新的数据a=10,a在Hash计算后结果为666,那么a就应该存放到1号Redis节点中。