前面我们提到的持久化,内存淘汰,管道等机制都是Redis在单个实例上的功能,但是随着系统并发量的不断升高,单个实例的redis就很难继续满足需求了.

为了满足高并发需求,Redis提供了主从(Redis Replication),哨兵(Redis Sentinel),集群(Redis Cluster)三种架构,分别应用于不同情景.

本文要介绍的就是主从架构,可以说,很多项目不一定会用到哨兵和集群,但是使用了redis的几乎都会用到主从.

主从的作用

读写分离!

读写分离!!

还是读写分离!!!

这个设计思路在前面体现过很多次了,可见其广泛的适用性.

主从读写分离

  • 在主从架构中,master(主节点)负责写,slave(从节点)负责读.
  • 每当master中的数据被修改,它就会把相同的命令同步(传播)到各个slave,以实现整个集群的数据一致性.
  • 这样master的读任务就会被各个slave分摊,写的效率就会提高,而读的效率可随着slave节点的增加而提高.

思考一个问题 : 为什么只能在master里写呢?多增加几个写节点(master)不就也提高了写的效率吗?

不行,因为redis并不支持主主架构.这个问题得从线程安全的角度去思考了.

我们把redis的每个节点都看成一个线程的话,原先单节点操作,也就是单线程,是不会有线程安全的隐患.但是拓展到了多节点,多个节点同时读写就需要考虑并发的问题了.

如果是一主多从,读与读操作不会有问题.但是多主的情况下,写与写就会有问题了,假设A,B两节点同时写同一个key,此时客户端访问不同的master,就会得到不一致的数据.其次A和B还得相互通知对方最新的数据,那么怎么才能确认谁的版本才是最新的呢?为了避免这种情况,就得加锁,写数据的时候双方就得频繁通信,以此确认对方没有在修改同一份数据,这样显然会消耗大量的CPU资源.

所以主主架构不仅没能提高性能,甚至还会带来数据不一致的问题.

主从的使用

可以通过slaveof命令来实现主从 : slaveof <host> <port>

比如有A,B两个节点,我们向A发送命令 : slave B 6379

那么B就会变成A的master,A就会完全复制B的数据.

我们对master的任何写操作都会同步到slave,在没有异常的情况下,主从保存的数据是一致的.

主从复制

先简要概括下复制的过程:

  • slave刚加入集群的时候,数据是空白的,因此要完全复制master的数据.
  • 第一次复制完成后,slave和master的数据就暂时一致了.此后,每当master有数据变动时,为了保持一致,也得把相应的改变同步到slave,这时只需要增量复制.

Redis的复制功能在2.8版本之后改进了一些地方,旧版本的命令是sync,新版本是psync.

这里我们先介绍旧版的,然后对比新版来体会下改进的地方.

旧版的复制功能分为同步(sync)和命令传播(command propagate)两个阶段.就是我们上面说的两步.

同步(全复制)

同步操作是由sync命令开启的:

  • slave向master发送sync命令.
  • master开始执行bgsave命令,生成一个rdb文件.与此同时还会使用一个缓冲区记录从现在开始执行的所有写命令(类似于aof).
  • 当master执行完bgsave后,就会将生成的rdb文件发给slave.
  • slave接收并载入这个rdb文件,从而将自己的数据库状态更新到与master执行bgsave之前的状态一致.
  • 然后master继续把缓冲区里记录的写命令发送给slave,slave执行这些命令,此时slave的状态就和master完全一致了.

sync

命令传播(增量复制)

在同步操作完成之后,master和slave就暂时达到了一致.不过master的数据会随时被修改,因此slave就得持续地与master进行同步.

所谓命令传播,就是 : master会将自己执行的每条写命令,发送给各个slave,这样它们执行完命令后,master与slave又会再次回到一致状态.

SYNC的缺陷

旧版本的问题在于没有考虑过由于网络抖动造成的断线重连问题.

由于网络原因造成master与slave暂时无法通信,那么master发送给slave的命令将会丢失.

此后,当slave重新连接master之后,由于断线期间的命令已经丢失,通过命令传播无法再次达到主从一致,所以slave只能再次执行sync,对master进行全复制才能再次达到一致.

sync需要生成RDB,是非常消耗CPU的.而且此前slave与master已经全复制过一次,数据大部分相同,断线的时间可能并不长,丢失的数据可能并不多,为了这少量丢失的数据而再次全复制,这显然不太合理.

新版PSYNC的实现

PSYNC分为完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式.

其中完整重同步与sync的同步的步骤是一样的.

而部分重同步则是新增,用于处理断线重连的情况 : 当slave重连时,如果条件允许,master只需把掉线期间的命令发给slave即可,不需要再次全复制.

注意,这里需要强调的是"条件允许",并不是所有重连上的slave都可以跳过全复制.

部分重同步的实现

先介绍三个名词:

名词 释义
offset 复制偏移量
backlog 复制积压缓冲区
run ID 服务器运行ID

offset

master和slave都会持有一个offset,作用相当于TCP中滑动窗口的sync,用来表示复制到了哪个位置.

假设master和slave目前的offset都是1000,然后master写入了一个33字节的数据,那么master的offset就会变成1033;然后master向slave发送这条写命令,slave的offset也会变成1033.

如果master和slave是一致的,那么它们的offset就会相同.

如果slave断线后重连,把offset发给master.master看见slave的offset小于自己的,就能明白slave是刚重连上来的.

backlog

前面我们说"并不是所有重连上的slave都可以跳过全复制.",就是和这个缓冲区backlog有关.

backlog由master维护,是个固定长度的队列(默认1mb),存储的是master最近执行过的写命令及其对应的offset.

为什么是固定长度呢?因为master会一直执行写命令,如果队列是无界的,那么最终会把redis的内存都占满了.

如果slave掉线重连,向master发送offset,然后到backlog里一查,发现offset还在队列里,那么master就能把队列里这个offset之后的所有命令发给slave以实现部分重同步.

但是由于队列的长度是固定的,如果slave掉线的时间太长,导致重连上来后无法在队列里找到对应的offset,那么它就只能进行完整重同步了.

复制积压缓冲区

run ID

主从服务器都拥有自己的,独特的run ID.

当slave连接上master时(不管是首次,还是重连),需要向master发送自己之前保存的run ID(也就是上次连接的master的ID),然后master拿这个和自己的比较.

如果相同,那么就说明slave是断线重连的,那么master就准备给它尝试部分重同步.

如果不相同,那么就说明slave是首次连接本服务器,就需要给它发送自己的run ID,然后进行完整重同步.

概念介绍完毕,接下来完整地看一遍psync的操作过程:

  • 如果slave此前未曾复制过任何服务器,或者之前执行过slaveof no one(从slave转变为master),当它开始新一次的复制时,会向master发送psync ? -1命令,主动请求进行完整重同步.
  • 如果slave已经复制过了某个服务器,那么它开始新一次的复制时,需要向master发送psync <runid> <offset>命令,master则根据两个参数来判断进行完整重同步还是部分重同步.
  • 如果master返回的是+fullresync <runid> <offset>,那么就进行完整重同步.
  • 如果master返回的是+continue,那么就进行部分重同步.
  • 如果master返回的是-err,就代表master的redis版本低于2.8,无法识别psync命令,那么slave只能向master发送sync来进行全复制.

psync