主从复制 - 数据备份, 读写分离, 手动高可用
- 负载均衡: 主结点只负责处理写请求, 从节点负责读请求;
- 主从复制, 主机挂了, 我们可以手动切换从机, 还可以搭配哨兵实现自动切换, 实现高可用;
- 需要注意的是, 在主从模式下, 假设进行同步的过程中主节点宕机了, 那么从节点此时还没有同步到所有的数据, 会发生数据不一致问题;
开启主从复制
通常有以下三种方式:
- 在 slave 直接执行命令:
replicaof <masterip> <masterport>
- 在 slave 配置文件中加入:
replicaof <masterip> <masterport>
- 使用启动命令:
-- replicaof <masterip> <masterport>
主从同步
主节点
从结点
全量同步
当我们配置好主从同步的时候,由于之前没有进行过任何同步,所以首先会进行一次全量数据同步到从库。
主从建立连接
Slave从库会主动和Master主库进行通信,发送psync 命令,该命令会捎带两个参数过去给Master,第一个参数是主库ID(runID),redis 在启动的时候,都会为自己生成一个ID,第二个是Slave复制Maser数据的偏移量offset。
Slave第一次和Master进行通信,由于一开始不知道Master的ID,所以传递了 ?;
由于是第一次复制,offset 传递 -1 表示第一次要进行全量复制。
接着Master接收到了Slave传递过来的命令以及相应的参数,一看是? 和 -1 ,那么就知道这个Slave要进行全量的复制,Master会给Slave 发送一个 fullresync 命令,告诉Slave接下来要开始全量复制,并带上自己的 ID,Slave 接收到这两个参数后会保存起来。
发送rdb文件
Master接着就会执行bgsave, fork 子进程,完成rdb文件的生成,生成完rdb文件后,会发送rdb文件给Slave,Slave会接收rdb文件,在进行接收之前,会先清空Slave自己的数据库数据【这个过程是阻塞的】,清空完成后,开始接收rdb文件,接收完成之后,就加载rdb文件到内存中。
这里还有一个问题,就是在接收rdb文件的时候,Master可能会有新的写操作过来,由于rdb是某一时刻的内存快照,所以之后的数据,是无法进行传输的;
这里redis采用了一个 replication_buffer 来解决,在生成rdb开始,新的写请求数据都会放到这个缓冲区一份,等待rdb传输完成之后,Master接着就会传输这个缓冲区的数据到 Slave,Slave开始接收,接收完成,主从数据保持一致了
**只要一个Slave和Master 建立好连接,对应的 replication_buffer 就会建立,如果断开连接,那么这个缓冲区就会释放。每个 Slave 都有一个replication_buffer **
增量复制
全量复制结束后, 就进入命令传播阶段, 通过一个长连接, 持续不断地将主库收到的写命令同步给从机;
命令传播阶段网络中断了,怎么办?
如果网络发生中断,最早的时候会再走一次全量复制。
后来对这个过程做了优化,采用增量复制的机制;
还记得全量复制的时候,会返回给Slave一个偏移量吗?其实Slave在接收数据之后,会增加这个偏移量来记录当前接收Master多少数据了。
如果网络发生了中断,就会重试和Master重新连接,连接之后,会发送自己的offset给Master,Master会根据Slave发送的偏移量来决定是给Slave做增量复制还是做全量复制。
从开始第一次主从复制开始,那么新的写请求在写入replication buffer的同时,也都会写入到一个叫做 repl_backlog_buffer 的缓冲区内,这是一个环形缓冲区,会记录Master接收的写命令和这条命令的偏移量(从第一次开始主从复制开始),这样Slave再重新连接之后,就可以从这里接着发送命令给Slave了。
所有 Slave 共用同一个 backlog_buffer!
注意连接没有断开的时候,这两个缓冲区是同时存在,如果连接断开,那么对应Slave的replication buffer缓冲区就会被删除。
其实就是环形的每段记录着当前命令和偏移量,随着当前写入的offset不断增大,就会覆盖之前的数据。
Master 会记录自己接收的写请求的最新偏移量, 当有 Slave 重连的时候, 与 Slave 的同步进度偏移量进行对比, 如果没有超过环形缓冲区长度, 可以用 backlog_ buffer 中的内容做增量复制, 否则做全量;
命令传播
全量同步完成后, 会进入命令传播阶段 ( 增量同步也是通过命令传播实现的 )
slave 默认会以每秒一次的频率,向 master 发送命令:REPLCONF ACK ,其中 reploff 是 slave 当前的复制偏移量。
发送REPLCONF ACK 命令对于主从服务器的作用:
1)检测 master 和 slave 的网络连接状态。
2)汇报自己的复制偏移量,检测命令丢失,master 会对比复制偏移量,如果发现 slave 的复制偏移量小于自己,会从 repl_backlog_buffer
向 slave 发送未同步的数据。
总结
到现在整个redis 主从复制的过程就讲解完成了,现在来做下总结。
主从同步分为两个类型:
全量同步
全量同步redis 会执行bgsave 来生成rdb文件,然后发送给从库,从库接收之前会先清空从库的数据空,防止之前有数据造成数据的污染,接收完rdb文件之后,就会就加载rdb文件到内存,这时同步其实并没有完成,在进行生成rdb文件的时候,还会有新的写请求过来,此时这些写请求会缓存在一个缓冲区内,这个缓冲区叫做 replication buffer
,当从库加载完rdb之后,就会接收这个缓冲区的所有写命令了,到此全量复制就结束了。
由于生产rdb是会阻塞主线程,这个过程很耗费资源,如果采用一个主多个从的方式,那么势必会增加主库的压力,所以从库不是越多越好;
增量同步
如果主从断开连接了,redis 主库会判断是进行全量复制还是增量复制,主库会根据从库发送过来的 runID 和 offset 判断,如果runID和主库的ID相同,并且主从的 offset 差距没有超过 repl_backlog_buffer 缓冲区的长度,就会复制 offset 之间的 repl_backlog_buffer 的命令给Slave。
两个缓冲区:
- replication buffer
replication buffer 是在从库和主库建立连接成功后创建的,在主从断开后,这个缓冲区也会被主库进行删除,主从库之间复制命令的传输,都会经过这个buffer,而且这个buffer是每个从库独有的。
- repl_backlog_buffer
主接收到第一个复制请求后,就会建立好这个buffer,这个buffer记录当前 Master 接收到的新的写操作命令 offset 和命令本身,是所有 Slave 公用的 buffer,Slave 发送psync之后,会和Master的offset进行比较,来决定是否进行增量复制。
注意点:
- Redis 单机内存越大越好吗? 不, 越大, Fork 子进程的时间越长, 执行 BGSAVE 和 BGREWRITEAOF 时主进程阻塞时间就越长; 主从同步的时候也会涉及到 BGSAVE;
- 为什么用 RDB 而不是 AOF ? 文件小, 恢复快;
- 如果想减少因为连接断开导致的全量同步, 可以适当增加 back_log 的大小;
- 如果 RDB 文件比较大, 或者网速比较慢, replication_buffer 会满, 主库会和从断开连接,删除buffer,如果从再来请求链接,可能会造成恶性循环。可以适当调大;
目前仍存在的问题
主机掉线后需要手动切换从机
- 可以用 Sentinel 解决;
主从过期时间不一致;
- 由于网络原理, 从机接收到写命令的时间会比主机晚, 使用 expire pexpire 命令设置过期时间的以后, 是以命令执行时间为起点的相对时间;
- 就会导致从机键值对的过期时间延迟; 可以使用 expireat, 设置绝对过期时间, 这里需要注意主从时间要同步;
从机挂掉后主机的runId 和 offset 会丢失; 只能全量同步;
- 改进: rdb 会记录 runId 和 offset, 这样从机从 rdb 恢复后, 如果来得及, 还是能增量的, 也不会丢失数据;
主机挂掉后, 剩下的从机, 都要重新和新选出来的主机做全量同步; 会有数据丢失, 且效率低下;
- PSYNC2.0 进行了改进, 原本的 runId 和 offset 都变成了两个, 这里简称为 id, id2, offset, offset2;
- 对于主机, id 是自己的id, id2 是上一任主机的id; 对于从机, id 是 现任主机id, id2 是上一任主机id;
- offset 是当前的偏移量, offset2 是上一任主机没挂的时候, 同步的偏移量;
- 发生主从切换后, 就可以通过 id2 知道原本都从属于同一个主机, 可以尝试进行增量同步;
数据量大, 写操作频繁时, 可能导致 replication_buffer 溢出, 无法解决海量数据的问题
- 用Redis集群解决;