
前阵子我们线上的一台Redis实例毫无征兆地OOM了进程直接没了。问题是那台机器是单节点既没有从库也没有像样的持久化保护缓存一挂后面的数据库瞬间被流量打满整个服务抖了差不多二十分钟。复盘时我越想越不甘心——主从复制这么基础的能力我竟然一直没有认真用过。于是那几天我把Redis主从复制从原理到实操重新过了一遍这篇就当是那段时间的清扫笔记。内容会覆盖主从复制到底在复制什么、全量同步和增量同步为什么能同时存在、Docker下怎么快速搭一主两从以及我踩过的几个坑和排查套路。1. 为什么需要主从复制先看清单机Redis的三条软肋1.1 单点故障再稳的机器也会翻车很多人用Redis默认就是单节点开一把梭因为Redis本身确实很稳定、很快但这不等于不会宕机。内存OOM、机器重启、磁盘故障、网络分区、运维误操作任何一个环节出问题单节点Redis都意味着那一整块缓存不可用。最典型的场景就是缓存崩了之后请求直接打到数据库上数据库扛不住流量跟着出问题。其实主从复制最大的价值恰恰不是提升性能而是“多放一份备份”。数据在Master和Slave各存一份Master要是挂了至少数据还在从节点上服务还能通过切换或继续读本来就存在的副本来续命。这个道理大家都懂但真到事故复盘时才会发现很多团队连这条保底都没做。1.2 读多写少场景下的能力瓶颈Redis是单线程模型所有命令按顺序执行CPU多核优势完全发挥不出来。当读写都压在同一节点上命令多了之后延迟会越来越明显。实际项目里Redis的负载往往是明显的读多写少比如热点商品、排行榜、用户会话这些基本都是大量读请求。主从复制带来的“读写分离”思路很直接写请求依然走Master读请求可以分散到Slave节点上。在一主多从的架构下读能力得到扩展Master也能从繁重的读压力中解脱出来把资源留给写命令。不过这里要泼一盆冷水主从复制扩的是“读”不是“写”。Master始终只有一个写命令全部集中在它身上所以如果你的场景是写多读少、或者写入量本身就很大主从复制帮不了你多少你得往Redis Cluster分片的方向去考虑。1.3 “数据安全”其实只有半条命Redis把数据存在内存里所以才有RDB快照和AOF日志这两种持久化方式。但注意持久化和“高可用”是两码事就算你定期生成RDB发生故障时还是会丢失最近一段时间的数据就算开了AOF恢复也需要时间在恢复完成前服务基本是不可用的。主从复制相当于是另一条数据保护通道。数据除了在Master落了盘还会实时传一份到Slave真正做到了跨节点冗余。哪怕Master的磁盘彻底坏了只要Slave是好的数据还在。1.4 主从复制能解决什么不能解决什么我把主从复制的边界整理成一张表照着它做架构决策能省很多弯路。能力主从复制是否解决备注数据冗余、防止单节点数据丢失解决多副本存储读能力水平扩展部分解决可通过一主多从扩展读写能力水平扩展不解决写命令仍集中在Master自动故障转移不解决需要Sentinel哨兵机制配合数据强一致性不解决默认是异步复制有延迟在线扩容、分片存储不解决需要Redis Cluster这张表是我搭副本前必须过一遍的检查项。特别是“自动故障转移”这一条很多初学者以为配了主从复制Master挂了系统还能自动切换这是最普遍的误解。主从复制只是把数据复制了谁来发现Master挂了、谁来选新的Master那是哨兵要做的事。2. 复制原理透彻版握手、全量同步与增量同步2.1 第一步从节点怎么认识主节点Redis主从复制从一条命令开始在从节点上执行REPLICAOF master_host master_port从节点就会主动去连接主节点并开始复制流程。在老版本里这命令叫SLAVEOFRedis 5.0以后官方把术语统一为Replica所以强烈建议新项目一律用REPLICAOF。整个连接建立过程不是简单发一条命令就行里面有几个子步骤从节点通过TCP连接主节点。从节点发送PING确保主节点可用。如果主节点配置了密码从节点需要发送AUTH进行认证这个密码就是后文要说的masterauth。从节点发送REPLCONF listening-port port告知主节点自己的监听端口。从节点发送PSYNC命令请求同步数据。PSYNC是Redis 2.8之后引入的复制协议核心它让Redis能区分“全量同步”和“增量同步”。没有它之前Redis 2.8只能用SYNC做全量复制效率非常低网络一抖动就全量重新拉数据这在生产环境是灾难。2.2 全量复制不是简单传文件那么简单首次建立复制、或者从节点离线太久导致积压数据过期时必须走全量复制。全量复制的完整流程是从节点发送PSYNC ? -1表示我不知道主节点的复制ID也没有历史偏移量请求全量同步。主节点收到后返回FULLRESYNC replid offset告诉从节点自己的复制ID和当前数据偏移量。主节点执行BGSAVEfork一个子进程生成RDB快照文件。在生成RDB的过程中主节点不会停止处理写命令而是把新写入的命令同时写入复制积压缓冲区。RDB生成完毕主节点通过网络把文件发送给从节点。从节点收到RDB后先清空自己内存里的旧数据再把RDB加载进来。主节点把复制积压缓冲区里积累的写命令发送给从节点。从节点执行这些命令追平主节点的数据状态。进入增量复制阶段主节点之后每执行一条写命令都会实时推送给从节点。这里有个关键的细节为什么全量同步要清空从节点的旧数据因为RDB是一份完整的内存快照从节点的旧数据可能是以前的历史数据如果不清空就加载就可能出现两个数据集重叠或冲突的问题。很多新手看到从节点启动后数据被清空第一反应是我配置错了其实这是全量复制的正常流程。另外一个大家容易忽略的地方是RDB传输期间主节点还会继续写数据这些数据不能丢所以必须有一个“复制积压缓冲区”把增量命令暂存起来。如果RDB文件很大、传输很慢而缓冲区太小装不下全部新命令那么即使RDB传完了从节点依然无法追平最后只能再次触发全量复制。2.3 增量复制让断线重连不再“伤筋动骨”如果每次网络抖动几秒钟都要全量同步一次那主从复制几乎没法在生产环境用。Redis 2.8的PSYNC机制就是为了解决这个问题。从节点在完成第一次全量复制后会保存主节点的replid和当前复制偏移量offset。之后如果从节点和主节点的连接断开重新连接时从节点会发送PSYNC replid offset告诉主节点我之前用的复制ID是这个我的数据位置在这里。主节点收到后会做两个判断replid是否匹配如果匹配说明从节点确实是从我这边复制过去的。offset是否还在复制积压缓冲区里如果还在说明从节点落后的数据没有超出缓冲区范围可以只把缓冲区内的增量命令发给它。如果两个条件都满足主节点返回CONTINUE然后把缺失的增量命令发送给从节点。这样一次网络抖动只需要补齐那么一小段数据效率远高于全量复制。如果主节点发现offset已经不在缓冲区里或者replid变了就只能返回FULLRESYNC让从节点再来一次全量复制。2.4 复制积压缓冲区决定增量同步能不能成功的“蓄水池”复制积压缓冲区本质是一个固定大小的环形队列由一个参数控制repl-backlog-size默认只有1MB。这个默认值其实相当小我们后面实操部分会详细说怎么调。缓冲区的工作方式是主节点每执行一条写命令都会把命令追加到缓冲区中同时继续把命令推送给从节点而从节点在断线重连时带着自己的offset来找主节点主节点只要确认这个offset还在缓冲区内即可。你可以把缓冲区想象成电梯里的垃圾桶每次只能装一定量的垃圾装满了就自动把最早的那些挤出去。如果从节点断线时间太长、落后太多等它回来时它对应的数据可能已经被挤出缓冲区那主节点就只能让它全量重新拉。所以repl-backlog-size的调节公式一般是repl-backlog-size 主节点平均每秒写入的数据量 × 预估最长断线重连时间 × 安全系数举例主节点平均每秒写1MB预计极端情况下从节点需要5分钟才能恢复连接那就是1MB × 300秒 300MB再乘1.5到2的安全系数600MB比较合适。这里最怕的是写入量很大的场景还沿用默认的1MB那几乎每次断线重连都会退化成全量同步。2.5 复制IDreplid主从关系的身份凭证replid是主节点在启动或成为主节点时生成的一个40字符随机字符串可以理解为Redis实例的“身份证号”。它的作用是标识每个主节点独有的复制历史。从节点第一次全量同步时会把主节点的replid保存下来后面做增量同步时它要带着这个replid去找主节点主节点一看对你是我儿子我可以给你增量。如果replid对不上比如原来的Master挂了管理员把一个Slave提升为新的Master此时新Master会生成新的replid那些想继续增量同步的节点就必须全量重新同步。还有一个常见坑如果Master重启它可能生成新的replid导致所有从节点都被迫做全量复制。这在数据量大时会非常痛苦所以很多生产环境都已经往哨兵模式迁移用故障切换来避免这类问题。如果只是用主从复制那就要特别注意别轻易重启Master。3. 动手实践Docker快速搭建一主两从3.1 为什么选择用Docker搭建Redis主从复制最快的方式一定是在Docker里跑。相比在本机编译安装Docker的优势是环境隔离、版本固定、网络编排方便。主从复制最关键的就是节点间网络连通Docker自定义网络可以让三个容器通过容器名互相访问省去很多IP配置的麻烦。另外一点是Docker镜像里的Redis版本可以固定我在搭建时用的redis:7.0生产环境建议只用大版本内的官方镜像不要用latest否则哪天镜像更新把行为变了线上环境会很难排查。3.2 编写三个节点的配置文件先建一个主节点配置redis-master.confport 6379 appendonly yes appendfilename appendonly.aof dir /data repl-backlog-size 50mb requirepass 123456 masterauth 123456解释一下几个关键配置appendonly yes开启AOF持久化主节点故障时数据恢复能力更强。repl-backlog-size 50mb把复制积压缓冲区调大到50MB演示环境下压力不大但仍展示了生产思路。requirepass主节点访问密码。masterauth虽然主节点不需要连接别人但以防它从故障中恢复后变成从节点提前配好认证密码。从节点配置文件redis-slave.confport 6380 appendonly yes dir /data replicaof redis-master 6379 masterauth 123456 requirepass 123456 replica-read-only yes核心区别是加了一条replicaof redis-master 6379指定主节点的地址和端口。这里的redis-master是Docker网络里的容器名稍后创建网络时会让容器名互通。masterauth是访问主节点时要用的密码必须和主节点的requirepass保持一致否则AUTH会失败复制根本建立不起来。第二个从节点配置一样把端口改成6381即可。3.3 启动容器并验证状态先创建网络和三个容器docker network create redis-net docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ -v $(pwd)/redis-master.conf:/redis.conf \ redis:7.0 redis-server /redis.conf docker run -d --name redis-slave1 \ --network redis-net \ -p 6380:6380 \ -v $(pwd)/redis-slave1.conf:/redis.conf \ redis:7.0 redis-server /redis.conf docker run -d --name redis-slave2 \ --network redis-net \ -p 6381:6381 \ -v $(pwd)/redis-slave2.conf:/redis.conf \ redis:7.0 redis-server /redis.conf启动完成后用Redis客户端进入主节点执行docker exec -it redis-master redis-cli -a 123456 info replication正常输出会显示role:master connected_slaves:2 slave0:ip172.18.0.3,port6380,stateonline,offset82,lag0 slave1:ip172.18.0.4,port6381,stateonline,offset82,lag0 master_replid:7b0a0d4c1e2f... master_repl_offset:82看到stateonline就说明复制已经建立。如果显示stateconnect或一直没有出现slave条目多半是认证失败、网络不通或配置里的replicaof写错了。3.4 实际验证读写分离的效果配置完成不等于万事大吉还需要从数据层面做一轮验证在主节点写入一个值SET user:1001 zhangsan。在从节点读取GET user:1001如果返回 zhangsan说明数据已经通过主从复制传过来了。尝试在从节点写入SET user:1002 lisi会收到错误READONLY You cant write against a read only replica这正是只读副本应该有的响应。如果后续要做读写分离应用层需要自己区分写连接和读连接分别指向Master和Slave地址。Docker演示环境里只要网络和配置无误这套流程跑通基本不会超过五分钟。4. 主从复制里那些不踩一次不会懂的细节4.1 从节点的read-only不是摆设Redis从节点默认是只读的这其实是一个保护机制防止有人误写入数据导致和Master数据不一致。如果你非要在从节点写数据有两种可能replica-read-only no被手动改成no从节点允许写入但这些写入不会同步回Master也不会同步给其他从节点。之后一旦发生全量复制从节点内存数据会被清空重建你写入的那些数据直接消失。所以生产环境一定要保持从节点只读并且做好权限控制。这个坑我在测试环境踩过一次有人在从节点上往里灌了一波测试数据后来主从切换新主节点启动后就带着那波脏数据排查了好久才发现是被改掉了只读开关。4.2 有密码的环境别忘记masterauthRedis的认证逻辑有点容易混淆从节点连接主节点时它需要用masterauth配置去完成AUTH而不是用自身的requirepass。如果主节点开启了requirepass而从节点没配masterauth日志里会不断出现MASTER - REPLICA sync started和Master did not reply to PING之类的报错复制永远处于连接中状态。排查时可以用docker logs redis-slave1看日志看到Unauthorized或NOAUTH Authentication required基本就是masterauth没配或者密码不对。4.3 从节点提供过期数据的问题可能有这样一个场景Master暂时无法连接从节点此时是否还能对外提供读服务这由replica-serve-stale-data控制默认是yes也就是说即使主从连接断了从节点依然可以处理读请求客户端读到的是断连前的数据。这个行为在业务上需要警惕。比如你缓存的是库存数据Master挂了从节点还在把旧库存返回给客户端看起来服务没挂但数据已经过期。所以有些团队会把replica-serve-stale-data设为no主从连接断开后直接拒绝读请求宁可让服务报错也不提供脏数据。具体怎么选取决于业务对数据新鲜度的容忍度。4.4 延迟从多少算异常主从复制默认是异步的从节点数据理论上有延迟。判断延迟最直接的方法是在Master和Slave上都执行info replication对比两个输出中的offsetmaster_repl_offset:1000 # Master侧 slave_repl_offset:1000 # Slave侧两者相等说明完全追平相差越多代表延迟越严重。还可以看slave条目里的lag字段正常情况下小于1秒。如果lag持续上涨说明网络或从节点处理能力有问题。排查方向一般是这几个主从节点之间网络带宽是否打满、从节点所在主机的CPU是否过高、是否有慢日志阻塞了从节点处理命令、RDB/AOF刷盘是否占了太多IO。多数情况下从节点靠得太远或网络抖动是延迟的主要原因尽量把主从放在同一个机房甚至同一内网。4.5 主从复制替代不了哨兵这个边界必须画清楚写到这里一定要强调一下主从复制只是“复制”不是高可用方案。高可用还需要Sentinel来做监控、通知和自动故障转移。Sentinel会周期性地向Master和Slave发送PINGMaster涉嫌宕机后Sentinel会通过投票机制从Slave中选择一个提升为新的Master再通知其他Slave去复制新的Master。这套机制才是生产环境真正需要的故障自愈能力。如果只是配了主从复制Master宕机后你只能手动找一台Slave执行REPLICAOF NO ONE把它升成Master其他从节点再指向它。这个过程少说也要几分钟业务早就扛不住了。所以生产环境至少是“主从复制 哨兵”数据量特别大或需要横向扩展时才考虑Redis Cluster。5. 常见问题与排查实录5.1 全量复制反复出现怎么看日志现象是主从复制状态一直在sync和online之间反复横跳过一段时间又从FULLRESYNC开始。原因通常有几个RDB文件太大传输时间长期间积压缓冲区装不下新写入导致追不上。网络不稳定传输中断。从节点磁盘写入慢加载RDB耗时太长。主节点频繁触发BGSAVE系统负载过高。排查先看主节点日志有没有Background saving terminated by signal再看从节点日志有没有SYNC with master in progress。如果确认是缓冲区太小调大repl-backlog-size如果是RDB太大考虑用repl-diskless-sync yes开启无盘复制减少磁盘IO压力让RDB直接在网络之间传输。5.2 主从数据不一致的几类原因主从复制做不到强一致这是设计使然。但如果在正常情况下长期不一致就要关注下面几个点从节点是否被手动设置了replica-read-only no并写入过数据这种脏数据会一直留在从节点。从节点是否加载了过期或损坏的RDB文件。是否用了主从复制但Master上又有本地的定时任务在写入后立即删除了key这个删除命令可能没有同步到从节点。大key同步太慢或者慢查询阻塞了从节点。数据一致性检查可以用redis-cli --bigkeys找大key然后对比Master和Slave的大小更严谨的做法是redis-full-check这类工具不过它主要是给集群环境用的主从环境重点看offset和lag就够了。5.3 主从复制、哨兵、Cluster怎么选这是新手最容易纠结的问题我按规模给一个简单的选型逻辑单机够用只想防宕机丢数据做主从复制。要求Master挂了能自动切换业务不能长时间中断主从复制加Sentinel。单节点内存已经存不下需要分片存储Redis Cluster。并发读很大写不太大一主多从加读写分离读压力大也能扛住。不管选哪种方案主从复制都是底层的基础哨兵和Cluster本质上也都依赖复制机制来保证数据在不同节点之间一致。把复制原理和配置搞明白后面学哨兵和Cluster会顺畅很多。5.4 主从故障转移后原Master回来怎么办如果运行过手动切换或哨兵切换原Master重新加入集群时会发现自己变成了从节点它会自动去复制新的Master。但不建议直接让它加入因为原Master停机期间可能产生了未同步的数据或配置冲突最稳妥的办法是先把它降级成从节点并指定新的Master确认同步状态稳定后再考虑是否恢复为读节点。实际操作中我曾经因为没做这个清理原Master启动后携带了旧数据导致一段时间内从节点读取出现新旧数据混杂后来只能停机重新全量同步才解决。5.5 快速排查清单现象可能原因快速处理复制一直没建立masterauth未配置检查从节点masterauth是否能认证state显示connect网络隔离或防火墙用容器内ping测试主从连通性频繁全量复制repl-backlog-size过小调大缓冲区或开启无盘复制从节点写入报READONLY从节点默认只读正常现象应用层区分读写连接主从offset持续差很多网络带宽或慢查询检查lag、网络延迟以及从节点CPU主节点重启后全量复制replid变化导致考虑迁移到哨兵模式排查问题的时候先看日志再看配置最后才怀疑网络这样效率最高。最后再分享一个我的个人习惯搭建完主从复制后不要只看info replication的online状态就收工一定要在Master写入、在Slave确认再用DEBUG sleep 5模拟主节点阻塞观察复制是否会中断、会不会自动恢复。这个演练做过一次就知道真实环境里主从复制遇到故障时是什么表现远比纸面理解深刻。希望这篇内容能帮你在自己环境里也把主从复制玩明白。