前面我们提到的持久化,内存淘汰,管道等机制都是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完全一致了.
命令传播(增量复制)
在同步操作完成之后,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
来进行全复制.