【Redis从入门到精通】第39篇:Redis主从复制——数据如何在主从节点间同步
上一篇【第38篇】serverCron——Redis的心跳定时任务干了哪些活下一篇【第40篇】旧版复制的硬伤——Redis 2.8之前为什么会反复全量同步假设你开了一家奶茶店生意火爆到一个人忙不过来。你请了个助手但是菜单数据只在你手里。客人点单你得自己查做完还得自己喊号助手只能站着发愣。这时你掏出一个小本本每次有菜单变更比如草莓断货了就抄一份给助手同步——这就是主从复制的雏形。Redis的主从复制也是这个道理一台机器顶不住加一台把数据复制过去。主从复制的三大用途在动手搭建之前先搞清楚为什么要主从主从复制的核心价值 ┌─────────────────────────────────────────────────────────┐ │ │ │ 1. 读写分离 2. 数据冗余 │ │ ┌──────┐ ┌──────┐ │ │ │ 主库 │ 写 │ 主库 │ 挂了 │ │ └──┬───┘ └──────┘ │ │ │ │ │ │ ┌──┴───────────────────┐ │ 从库顶上 │ │ │ │ │ │ ▼ │ │ ▼ ▼ ▼ │ ┌──────┐ │ │ 从1 从2 从3 │ │ 从库 │ 数据还在 │ │ (读) (读) (读) │ └──────┘ │ │ │ │ │ 3. 故障恢复 │ │ │ 主库挂了 → 选一个从库 → │ │ │ 提升为新主库 → 服务恢复 │ │ └─────────────────────────────────────────────────────────┘读写分离主库负责写多个从库分担读压力。如果你的业务是读多写少大部分互联网应用都是这样加几个从库就能线性扩展读吞吐量。数据冗余相当于数据有了异地备份。主库的数据完整地拷贝到了从库主库挂了至少数据还在。故障恢复主库宕机后可以从从库中选一个提升为新的主库业务切换到新主库继续运行。这是Redis高可用的基础配合Sentinel可以实现自动故障转移。主从复制的拓扑结构Redis支持灵活的拓扑组织方式拓扑结构对比 【一主多从】 【级联复制】 ┌─────────┐ ┌─────────┐ │ 主库 │ │ 主库 │ └──┬──┬───┘ └───┬─────┘ │ │ │ │ ┌──▼──┐ ┌──▼──┐ ┌──▼──┐ ┌───▼───┐ │ 从1 │ │ 从2 │ │ 从3 │ │ 从1 │ └─────┘ └─────┘ └─────┘ └───┬───┘ │ ┌─────▼─────┐ │ 从1-子 │ ← 从库的从库 └───────────┘ 优点管理简单、主库负载低 优点减轻主库的复制压力 缺点所有复制流量走主库 缺点链路越长延迟越大一主多从适合大多数场景。级联复制适合从库非常多的情况比如几十个从库让主库只复制给几个一级从库再由它们分发给更多的二级从库。搭建你的第一个主从复制最简配置最少两台机器或同一台机器两个端口配置文件只需要一行# 从库的redis.conf replicaof 192.168.1.100 6379 # 或者旧版语法 # slaveof 192.168.1.100 6379注意replicaof是 Redis 5.0 引入的新命令用它替代了带有历史包袱的slaveof。老版本2.8~4.x只能用slaveof。功能上两者完全等价。带认证的配置生产环境一般都会设置密码# 主库 redis.conf requirepass mypassword123 # 从库 redis.conf masterauth mypassword123 # 从库用来连接主库的密码 replicaof 192.168.1.100 6379如果主库设置了requirepass从库必须配置对应的masterauth否则连接会被拒绝。从库只读# 查看从库只读设置redis-cli CONFIG GET replica-read-only# 1) replica-read-only# 2) yes# 从库默认不接受写命令redis-cli-p6380SETtestvalue# (error) READONLY You cant write against a read only replica.这是很好的安全设计——如果业务代码不小心向从库发了写请求Redis会直接拒绝避免主从数据不一致。运行时切换主从关系不需要重启REPLICAOF命令可以动态地改变主从关系# 让当前节点成为192.168.1.100:6379的从库redis-cli-p6380REPLICAOF192.168.1.1006379# 让当前节点脱离主从关系升级为独立主库redis-cli-p6380REPLICAOF NO ONEREPLICAOF NO ONE在故障转移时特别有用——主库挂了在合适的从库上执行这个命令它就成了新的主库可以接收写请求。复制过程的两个阶段主从复制不是一次性把数据搬过去就完了它分为两个相辅相成的阶段复制两阶段 ┌──────────────────────────────────────────────────────┐ │ │ │ Phase 1初次同步全量复制 │ │ ┌──────┐ ┌──────┐ │ │ │ 主库 │ ═══ RDB文件 ═══→ │ 从库 │ │ │ └──┬───┘ └──┬───┘ │ │ │ │ │ │ │ ① 主库fork出子进程 │ │ │ │ ② 子进程生成RDB快照 │ │ │ │ ③ RDB发给从库 │ ④ 从库清空自身数据 │ │ │ ⑤ 主库把传输期间 │ ⑥ 从库载入RDB │ │ │ 的写命令缓存发过去 │ ⑦ 从库执行缓存命令│ │ │ │ │ │ Phase 2增量同步命令传播 │ │ ┌──────┐ ┌──────┐ │ │ │ 主库 │ ═══ 写命令 ═══→ │ 从库 │ │ │ │ SET │ ┌──→│ SET │ │ │ │ DEL │ │ │ DEL │ │ │ │ INCR │ │ │ INCR │ │ │ └──────┘ │ └──────┘ │ │ │ │ │ ← 异步主库不等从库确认 ← │ │ │ └──────────────────────────────────────────────────────┘Phase 1全量复制初次握手当从库第一次连接到主库时主从之间数据完全不一致必须做一次全量同步。这是整个复制过程中最重的操作从库向主库发送PSYNC ? -1首次同步的特殊标记主库收到后执行BGSAVE生成RDB快照RDB生成期间主库把收到的所有写命令记录到一个复制缓冲区里RDB文件生成完毕主库把文件发送给从库从库收到RDB后先清空自身所有数据然后载入RDB载入完成后主库把复制缓冲区中的命令发送给从库从库执行这些命令追平RDB生成到传输完成之间的时间差全量复制完成进入增量复制阶段这个阶段的时间开销取决于数据量。一个简单的估算公式数据量RDB生成时间RDB传输时间从库恢复时间总计估算100MB1~3s1~5s千兆网5~10s7~18s1GB5~15s10~30s30~60s45~105s10GB30~60s60~180s5~10min7~13min100GB3~10min10~30min30~60min45~100minPhase 2增量复制命令传播全量复制完成后主从之间通过命令传播保持数据一致。主库每执行一条写命令就把这条命令广播给所有从库// propagate() 简化逻辑voidpropagate(structredisCommand*cmd,intdbid,robj**argv,intargc){// 1. 如果开启了AOF把命令追加到AOF缓冲区if(server.aof_state!AOF_OFF)feedAppendOnlyFile(cmd,dbid,argv,argc);// 2. 如果有从库把命令追加到复制缓冲区if(listLength(server.slaves))replicationFeedSlaves(server.slaves,dbid,argv,argc);}异步复制的代价一个需要认真对待的事实主库不等待从库确认写命令。主库执行完写命令后立即返回给客户端复制操作在后台异步进行。这带来的好处是从库的网络延迟不会影响主库的写入性能。但代价是异步复制的数据丢失风险 时间线 t0: 客户端 SET key important_data t1: 主库执行成功返回OK给客户端 t2: 主库把命令发送给从库异步 t3: 主库宕机天有不测风云 结果从库永远不会收到SET key important_data important_data 永远丢失 数据丢失窗口 ≈ 主库宕机那一刻到从库追平数据的时间差复制延迟监控因为异步复制从库的数据天然比主库老一点。这个延迟有多大呢# 在主库上查看复制状态redis-cli INFO replication# 输出示例# # Replication# role:master# connected_slaves:2# slave0:ip192.168.1.101,port6379,stateonline,offset1234567,lag1# slave1:ip192.168.1.102,port6379,stateonline,offset1234560,lag3# master_repl_offset:1234567# 在从库上查看redis-cli-p6380INFO replication# 输出示例# # Replication# role:slave# master_host:192.168.1.100# master_port:6379# master_link_status:up# master_last_io_seconds_ago:0# master_sync_in_progress:0# slave_repl_offset:1234567# slave_priority:100# slave_read_only:1关键字段解读字段含义健康判断master_repl_offset主库的复制偏移量它是一个全局递增的计数器对比slave的offsetslave_repl_offset从库当前的复制偏移量master_repl_offset - slave_repl_offset就是延迟的数据量lag从库延迟秒数0表示几乎无延迟lag 5就值得关注了master_link_status主从连接状态必须是upmaster_sync_in_progress是否正在全量同步应该是0除非正在初始化# 计算复制延迟数据量redis-cli-hmaster INFO replication|grepmaster_repl_offset|cut-d:-f2# 1234567redis-cli-hslave INFO replication|grepslave_repl_offset|cut-d:-f2# 1234467# 延迟 1234567 - 1234467 100字节几乎可以忽略WAIT命令——给异步复制加一点同步如果你有一笔绝对不能丢的写入可以用WAIT命令# SET之后等待至少1个从库确认复制超时1000msSET important_keycritical_valueWAIT11000# (integer) 1 ← 1个从库已确认# 如果超时了还没等到足够确认数WAIT2100# (integer) 1 ← 只有1个从库确认不满足2个的要求WAIT的语义第一个参数numreplicas需要等待多少个从库确认第二个参数timeout超时时间毫秒返回值实际确认的从库数量可能小于numreplicas踩坑提示WAIT不是原子的SET和WAIT是两条独立的命令。在它们之间主库仍可能宕机。如果需要强一致性考虑使用Redis Sentinel 客户端确认机制或者直接上Redis Cluster。更重要的是——WAIT会让当前客户端连接阻塞它会阻塞整个Redis的事件循环单线程直到超时或足够多从库确认。在高并发场景下慎用。读写分离中的注意事项读写分离听起来很美好——主库写、从库读、吞吐翻倍。但实现起来有几个容易踩的坑读写分离的童话 vs 现实 童话 现实 ┌──────┐ ┌──────┐ │ 主库 │──→写 │ 主库 │──→写 (100%) └──┬───┘ └──┬───┘ │ │ 异步复制 ┌──┴────────────┐ ┌────┴──────────┐ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ 从1 从2 从3 从1 从2 从3 (33%) (33%) (34%) (延迟) (延迟) (延迟) 刚写入的数据查不到问题一数据延迟客户端刚刚写入主库立刻去从库读——可能读不到因为异步复制有延迟。应对策略对实时性要求高的场景如扣减库存后立即查询余额读写都走主库。延迟可接受的场景如推荐文章列表、历史订单走从库。使用WAIT命令强制等待复制后再去从库读。问题二从库故障如果某个从库挂了客户端必须能感知并切换到其他从库或主库// 伪代码连接池分离读写classRedisPool{JedisPoolmasterPool;// 主库连接池写ListJedisPoolslavePools;// 从库连接池读Stringget(Stringkey){// 随机选一个健康的从库JedisPoolpoolselectHealthySlave();if(poolnull){// 所有从库都挂了降级到主库读poolmasterPool;}returnpool.getResource().get(key);}Stringset(Stringkey,Stringvalue){// 写永远走主库returnmasterPool.getResource().set(key,value);}}问题三主从数据不一致如果主库用了RANDOMKEY或SPOP等随机的命令主从之间执行结果可能不同。虽然Redis主从复制传播的是命令不是结果保证了确定性命令的一致性但你得清楚哪些命令在主从上表现不同。总结主从复制是Redis高可用的基石理解它需要记住几个关键点维度要点三大用途读写分离、数据冗余、故障恢复拓扑结构一主多从简单、级联复制减轻主库压力配置从库replicaof一行搞定认证加masterauth复制阶段全量复制RDB 增量复制命令传播异步特性主库不等从库确认可能丢数据延迟监控INFO replication中的offset和lagWAIT命令给异步复制加了一层半同步确认读写分离注意数据延迟、从库故障切换、一致性保证主从复制看起来不复杂但要做到生产级稳定运行还需要理解复制协议的演变——这就要说到旧版SYNC的那些硬伤了。上一篇【第38篇】serverCron——Redis的心跳定时任务干了哪些活下一篇【第40篇】旧版复制的硬伤——Redis 2.8之前为什么会反复全量同步