复制
通过持久化功能,Redis保证了即使在服务器重启的情况下也不会损失(或少量损失)数据。但是由于数据是存储在一台服务器上的,如果这台服务器的硬盘出现故障,也会导致数据丢失。为了避免单点故障,我们希望将数据库复制多个副本以部署在不同的服务器上,即使有一台服务器出现故障其他服务器依然可以继续提供服务。这就要求当一台服务器上的数据库更新后,可以自动将更新的数据同步到其他服务器上,Redis提供了复制(replication)功能可以自动实现同步的过程。
同步后的数据库分为两类,一类是主数据库(master),一类是从数据库(slave)。主数据库可以进行读写操作,当发生写操作时自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
添加从数据库
使用命令
开启一个Redis服务实例
redis-server ./redis.conf
使用命令简单添加实例的从服务
redis-server --port 6380 --slaveof 127.0.0.1 6379
使用配置
在从服务器的配置文件redis-6380.conf添加配置
slaveof <mater-id> <master-port>
服务器信息
连接到主服务器,使用info replication命令查看6379的角色role为master(主数据库),连接的从数据库connected_salves为1。
redis-cli -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=70,lag=0
master_replid:e2bc3db13b3e91ec2ea0daf130768fc8184b8052
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
连接到从服务器,使用info replication命令查看6379的角色role为salve(从数据库)。
[root@centos7 ~]# redis-cli -p 6380
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:10
master_sync_in_progress:0
slave_repl_offset:154
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:e2bc3db13b3e91ec2ea0daf130768fc8184b8052
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
可以通过设置从数据库的配置文件中的slave-read-only为no使从数据库可写,但是尽量不要使用,因为没有意义。
除了通过配置文件和客户端来设置slaveof参数,还可以在运行时使用slaveof命令更换主数据库,如果当前数据库已经有了主数据库,那么就会停止和原来的主数据库同步而和我们新指定的主数据库同步。
原理
1、初始化连接阶段
当从数据库启动后,会向主数据库发送SYNC命令,主数据接收到SYNC命令后开始在后台保存快照(即RDB持久化过程),并将保存期间收到的客户端命令缓存起来。当快照完成后,Redis将快照文件和所有缓存的命令发送给从数据库。从数据库收到后,会载入快照文件并执行收到的缓存的命令。Redis使用Tcp协议通信,我们使用telnet工具伪装为从数据库来了解同步的过程。
连接主数据库
telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
发送PING命令确认主数据库是否可以连接
PING
+PONG
主数据库回复+PONG,说明已经连上了主数据库
如果主数据库需要密码才能连接,我们发送AUTH命令进行验证,而后向主数据库发送REPLCONF命令说明自己的端口号
REPLCONF listening-port 6380
2、数据同步阶段
从数据库发送SYNC命令开始数据同步
aof-preamble▒▒▒msg2worldmsg3hellomeshellofoo▒msghellomsg1helloǺ▒'xtermxtermxtermxtermxtermxtermxterm
*1
$4
PING
*2
$6
SELECT
$1
0
*3
$3
set
$4
msg3
$5
china
从数据库会将收到的内容写入到硬盘上的临时文件,当写入完成后从数据库将这个临时文件替换REDB快照文件,之后的操作与RDB持久化时启动恢复的过程一样。在同步的过程中,从数据库并不会阻塞,而是继续处理客户端发来的命令。默认情况下,从数据库会用同步前的数据对命令进行响应。可以配置slave-serve-stale-data参数为no来使从数据库在同步完成前对所有命令(除INFO和SLAVEOF)都回复错误:“SYNC with master in progress.”
之后主数据的任何数据变化都会同步给从数据库,同步的内容和Redis通信协议一样。
例如
主数据库客户端输入
127.0.0.1:6379> set hell world
OK
127.0.0.1:6379>
从数据库客户端端返回
*3
$3
set
$4
hell
$4
world
3、心跳检测阶段
当数据同步完成以后,在此后的时间里主从维护着心跳检查来确认对方是否在线,每隔一段时间(默认10秒,通过repl-ping-slave-period参数指定)主数据库向从数据库发送PING命令判断从数据库是否在线,而从数据库每秒1次向主节点发送REPLCONF ACK命令,命令格式为:REPLCONF ACK {offset},其中offset指从数据库保存的复制偏移量,作用一是汇报自己复制偏移量,主数据库会对比复制偏移量向从节点发送未同步的命令,作用二在于判断主数据库是否在线,从库接送命令并执行,最终实现与主库数据相同
从服务器客户端
*1
$4
PING
*1
$4
PING
…
全量、增量复制
Redis2.6之前,在数据同步阶段,从数据库会向主数据库发送SYNC命令。同时主数据库接收到SYNC命令后会执行bgsave命令在后台保存快照(即RDB持久化的过程),并将保存快照期间接收到的命令缓存起来。当快照完成后,Redis会将快照文件和所有缓存的命令发送给从数据库。从数据库收到后,会载入快照文件并执行收到的缓存的命令。这叫做全量复制。
但这时会出现一个问题,当主从数据库之间的连接断开重连后,会重新进行复制初始化,即使从数据库只是有几条命令没有收到,主数据库也必须要将数据库的所有数据重新传输给从数据库。这使得主从数据库断线重连后的数据恢复过程效率很低下。
Redis2.8版的一个重要改进就是断线重连能够支持有条件的增量数据传输。当从数据库重新连接上主数据库后,主数据库只需要将断线期间执行的命令传送给从数据库,从而大大提高Redis复制的实用性。
增量复制的几个概念。
1、offset(复制偏移量):
主库和从库分别各自维护一个复制偏移量(可以使用info replication查看),用于标识自己复制的情况,在主库中代表主库向从库传递的字节数,在从库中代表从库同步的字节数。每当主库向从库发送N个字节数据时,主库的offset增加N,从库每收到主库传来的N个字节数据时,从库的offset增加N。因此offset总是不断增大,这也是判断主从数据是否同步的标志,若主从的offset相同则表示数据同步量,不同则表示数据不同步。
2、replication backlog buffer(复制积压缓冲区):
复制积压缓冲区是一个固定长度的FIFO队列,大小由配置参数repl-backlog-size指定,默认大小1MB。需要注意的是该缓冲区由master维护并且有且只有一个,所有slave共享此缓冲区,其作用在于备份最近主库发送给从库的数据。
在主从命令传播阶段,主库除了将写命令发送给从库外,还会发送一份到复制积压缓冲区,作为写命令的备份。除了存储最近的写命令,复制积压缓冲区中还存储了每个字节相应的复制偏移量,由于复制积压缓冲区固定大小先进先出的队列,所以它总是保存的是最近redis执行的命令。
3、run_id(服务器运行的唯一ID)
每个redis实例在启动时候,都会随机生成一个长度为40的唯一字符串来标识当前运行的redis节点,查看此id可通过命令info server查看。
当主从复制在初次复制时,主库将自己的runid发送给从库,从库将这个runid保存起来,当断线重连时,从库会将这个runid发送给主库。主库根据runid判断能否进行部分复制:
如果从库保存的runid与主节点现在的runid相同,说明主从库之前同步过,主库会根据offset偏移量之后的数据判断是否执行部分复制,如果offset偏移量之后的数据仍然都在复制积压缓冲区里,则执行部分复制,否则执行全量复制;如果从库保存的runid与主库现在的runid不同,说明从库在断线前同步的redis节点并不是当前的主库,只能进行全量复制。
-如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步);
- 相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量。
接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作,如何判断已经在介绍runid时进行详细说明。
根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:
(1)如果主服务器返回+FULLRESYNC <runid> <offset>回复,那么表示主服务器将与从服务器执行完整重同步操作:
其中runid是这个主服务器的运行ID,从服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;
而offset则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。
6380脱离6379之前,6379的信息
6379的runid:e2bc3db13b3e91ec2ea0daf130768fc8184b8052
6380脱离6379之前,6380的信息
脱离6379的从服务
127.0.0.1:6380> slaveof no one
6380脱离6379之后的信息
Master_replid2变为了原来6379的runid:e2bc3db13b3e91ec2ea0daf130768fc8184b8052
Master_replid为新的runid
6379修改为6380的从数据库
127.0.0.1:6379> slaveof 127.0.0.1 6380
OK
6379作为6380的从数据库信息,
Master_replid2变为6379原来自己的runid:e2bc3db13b3e91ec2ea0daf130768fc8184b8052
Master_replid为新的runid,也是6380的runid:bef8fdd800a980a50d4806833eb428bf96e77d63
为了触发全量复制,这里将复制积压缓冲区的大小设置成了较小的大小
127.0.0.1:6380> config set repl-backlog-size 36172
接下来使用telnet模拟一个从数据库,
[root@centos7 ~]# telnet 127.0.0.1 6380
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
PING
+PONG
当主数据库回复+PONG
键入:PSYNC e2bc3db13b3e91ec2ea0daf130768fc8184b8052 1
发现主数据库执行了全复制
(2)如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分同步操作,
从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了;
模拟主数据库此时offset为7226,然后我们键入
set hello world
set msg hell
set msg2 hi
使用telnet模拟从数据库增量复制主数据库的数据
首先连接到主数据库键入命令PING,主数据库返回PONG
连接后,我们键入PSYNC e2bc3db13b3e91ec2ea0daf130768fc8184b8052 7226
我们发现主数据返回+CONTINUE,说明使增量复制,接下来我们看到了
*3
$3
set
$5
hello
$5
World
…
确实是增量复制
(3)如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis 2.8,它识别不了PSYNC命令,
从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。
由此可见psync也有不足之处,当从库重启以后runid发生变化,也就意味者从库还是会进行全量复制,而在实际的生产中进行从库的维护很多时候会进行重启,而正是有由于全量同步需要主库执行快照,以及数据传输会带不小的影响。因此在4.0版本,psync命令做了以下改进。
redis4.0新版本除了增加混合持久化,还优化了psync(以下称psync2)并实现即使redis实例重启的情况下也能实现部分同步,下面主要介绍psync2实现过程。psync2在psync1基础上新增两个复制id(可使用info replication 查看):
master_replid:一个长度为41个字节(40个随机串+’0’)的字符串,每个redis实例都有,和runid没有直接关联,但和runid生成规则相同。当实例变为从实例后,自己的master_replid会被上一个主实例的master_replid覆盖。
master_replid2:复默认初始化为全0,用于存储上次主实例的master_replid。
在4.0之前的版本,redis复制信息完全丢失,所以每个实例重启后只能进行全量复制,到了4.0版本,仍然可以使用部分同步,其实现过程:
1、存储复制信息
redis在关闭时,通过shutdown save,都会调用rdbSaveInfoAuxFields函数,把当前实例的repl-id和repl-offset保存到RDB文件中,当前的RDB存储的数据内容和复制信息是一致性的可通过redis-check-rdb命令查看。
2、重启后加载RDB文件中的复制信息
redis加载RDB文件,会专门处理文件中辅助字段(AUX fields)信息,把其中repl_id和repl_offset加载到实例中,分别赋给master_replid和master_repl_offset两个变量值,特别注意当从库开启了AOF持久化,redis加载顺序发生变化优先加载AOF文件,但是由于aof文件中没有复制信息,所以导致重启后从实例依旧使用全量复制!
3、向主库上报复制信息,判断是否进行部分同步
从实例向主库上报master_replid和master_repl_offset+1;从实例同时满足以下两条件,就可以部分重新同步,否则执行全量同步:
满足部分重新同步的两个必要条件:
1)、从实例上报master_replid串,与主实例的master_replid或master_replid2有一个相等,用于判断主从未发生改变;
2)、从实例上报的master_repl_offset+1字节,还存在于主实例的复制积压缓冲区中,用于判断从库丢失部分是否在复制缓冲区中;
psync2除了解决redis重启使用部分同步外,还为解决在主库故障时候从库切换为主库时候使用部分同步机制。redis从库默认开启复制积压缓冲区功能,以便从库故障切换变化master后,其他落后该从库可以从缓冲区中获取缺少的命令。该过程的实现通过两组replid、offset替换原来的master runid和offset变量实现:
- master_replid和master_repl_offset:如果redis是主实例,则表示为自己的replid和复制偏移量; 如果redis是从实例,则表示为自己主实例的master_replid和同步主实例的复制偏移量。
- master_replid2和second_repl_offset:无论主从,都表示自己上次主实例master_replid和复制偏移量;用于兄弟实例或级联复制,主库故障切换psync。
- 判断是否使用部分复制条件:如果从库提供的master_replid与master的master_replid不同,且与master的master_replid2不同,或同步速度快于master; 就必须进行全量复制,否则执行部分复制