
面试官问出“Redis 宕机了锁不就丢了吗”这句话时其实是在考察你有没有真正理解分布式锁的边界条件。很多人的第一反应是“那用 Redisson 啊有看门狗自动续期”但这个回答在面试官面前往往只能得个及格分。要真正把这个问题答透需要把 Redis 分布式锁从“一把普通的锁”到“一套在异常场景下依然成立的共识机制”整个链路想清楚。这篇文章我会从最基础的 SET NX EX 说起把持久化、主从复制、哨兵、集群、RedLock、看门狗这些高频词逐个串起来顺带给出我在实际项目中踩过的坑和排查经验。1. 先别急着答“会丢”这个问题的本质在哪很多人拿到这道面试题后的第一反应是背答案Redis 宕机确实会丢锁所以要用持久化、用主从、用 RedLock。这种回答方向没错但缺少一个关键的前置思考——面试官问的“丢锁”到底丢的是什么锁在什么场景下丢丢了之后会造成什么后果1.1 分布式锁到底在锁什么在单机应用里多线程抢同一个资源我们靠 synchronized 或者 Lock 就能解决。但到了分布式环境下多个服务的多个实例各自持有自己的 JVM 锁彼此看不到对方根本无法互斥。这时候就需要一个“大家共同信任的第三方”来记录谁拿了锁——Redis 就是扮演这个第三方的。你在 Redis 里写入一个 key比如lock:order:12345谁写成功谁就持有锁。其他节点看到这个 key 存在就认为自己拿不到锁于是等待或者重试。这个模型听起来很简单但它隐含着三个前提第一Redis 本身要可靠第二Redis 的 key 过期时间要设计合理第三业务执行时间要小于锁的过期时间。分布式锁的可靠性从来不是“能不能加锁”的问题而是“锁在异常情况下还能不能守住互斥性”的问题。宕机只是异常情况里最极端的一种。1.2 Redis 在分布式锁里的定位与短板Redis 之所以成为分布式锁的主流选择是因为它够快、够简单、生态够成熟。单线程模型天然保证了对同一个 key 的操作是原子的配合SET lock:key token NX PX 30000这种原子命令可以做到“加锁 过期时间”一步完成。但 Redis 的“快”是建立在内存上的内存就意味着断电全丢宕机重启后数据可能消失。这个问题在缓存场景下可以忍受顶多重新查一次数据库;但在分布式锁场景下就变成致命的——锁丢了就等于临界区的大门被打开了多个节点同时进入原本应该串行的业务变成并行可能产生超卖、重复扣款、重复下单等严重事故。短板就在这Redis 的定位是缓存分布式锁只是它顺手承接的一个扩展功能不是它设计时就针对高可靠场景打磨过的能力。所以“Redis 宕机锁就丢了”这句话本质上是在问“如何在一个为性能而设计的系统里尽量做出可靠性的保障”。2. 从一把“朴素锁”看 Redis 宕机究竟会发生什么要分析宕机带来的问题得先搞清楚最基础的那把锁是怎么实现的。很多人写分布式锁都是直接从网上抄一段 SET NX EX 的代码然后封装成工具类。这样用没问题但面试官追问几个细节就容易卡壳。2.1 最常用的 SET NX EX 到底做了什么先看最简单的一种加锁实现SET lock:order:12345 uuid-xxxx NX PX 30000这行命令拆开来看NX表示只有 key 不存在时才设置成功这就保证了互斥PX 30000表示锁的过期时间是 30 秒防止持有锁的节点挂了导致死锁value 用 uuid是为了释放锁时能校验“这是我自己的锁”。释放锁的时候标准的做法是先 Lua 脚本校验 value再删除 keyif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end为什么不能用两条命令先 get 再 del原因很简单两条命令之间存在时间差。如果 get 校验通过后锁刚好过期了、被别的节点抢走这时候再执行 del 就会把别人的锁删掉这叫误删锁。Lua 脚本保证了校验和删除是原子的。这套实现在 Redis 单实例正常运行的情况下是完全成立的。它的问题只出现在“异常”里。2.2 单点宕机的三种丢失场景我把单机 Redis 宕机可能造成锁丢失的场景拆成三种方便面试时分层回答。第一种是进程崩溃但机器没挂。Redis 进程异常退出重启后如果没开持久化内存里的数据全部清空锁自然就没了。这种情况下原本持有锁的节点 1 还在执行业务节点 2 发现锁不存在也能成功加锁两个节点同时进入临界区。第二种是机器宕机换了新机器。如果 Redis 没做持久化或者持久化的 RDB 文件是几分钟前的快照重启后锁一样丢失。这里有个容易被忽略的细节即使配置了 AOF 追加持久化如果 fsync 策略是 everysec最多也会丢一秒的数据。第三种是 Redis 所在节点发生网络分区。客户端和 Redis 之间断了但 Redis 本身还活着。这其实是“客户端视角的宕机”客户端以为自己拿不到锁了但锁还在 Redis 里等网络恢复后会出现混乱。这三种场景面试时只需要讲清楚前两种就足够展示深度了。核心结论是单机 Redis 的分布式锁在宕机时锁会丢这是事实不是设计 bug。2.3 排“宕机锁丢”的最快路径如果只是想在项目里快速避免锁丢失最直接的做法是给 Redis 开启持久化尤其是 AOF 的那种 appendfsync everysec 级别。这样即使 Redis 宕机重启锁的数据也能从持久化文件恢复丢失窗口控制在 1 秒以内。但这只是第一步。因为持久化解决的是“重启后数据还在”的问题解决不了“主从切换时锁被覆盖”的问题。这也是下面要高能的部分主从结构里的分布式锁才是真正的重灾区。3. 持久化与高可用让锁“活过”宕机聊到 Redis 高可用很多人的知识就停留在“配个主从、加个哨兵”这个层面。但真到分布式锁的场景下主从复制带来的一致性问题是很容易被忽略的。3.1 RDB 与 AOF重启用持久化兜底RDB 是 Redis 默认的快照持久化方式每隔一段时间把全量数据存到磁盘。配置 save 900 1 表示 15 分钟内至少有一次 key 变更就触发一次快照。问题在于两次快照之间的数据是丢失的。假设你 14 分 59 秒加的锁宕机前 Redis 还没来得及做快照重启后锁就没了。AOF 则是以日志形式追加每个写操作appendfsync 设置为 always 时每次写操作都会刷盘理论上最多丢一条命令。但 always 性能损耗很大线上 Redis 一般配置 everysec结合 Redis 官方给的默认值宕机时最多丢失 1 秒的写操作。在生产环境我建议锁的关键数据用 AOF everysec兼顾性能和数据恢复。但要注意持久化解决的是“Redis 重启恢复”的问题对“主节点宕机后从节点顶上”的场景作用接近于零。因为主从复制的数据同步是异步的。3.2 主从复制下锁的秘密丢失主从架构出现的原因很简单单点 Redis 一旦宕机整个分布式锁不可用所有业务都得阻塞等待。于是配了一主一从主节点负责写从节点负责读和故障接管。但问题也跟着来了。Redis 主从复制默认是异步的。主节点收到加锁请求后返回给客户端“加锁成功”同时异步向从节点同步数据。假设这个同步还没完成主节点突然宕机了哨兵把从节点提升为主节点——此时从节点上根本没有这把锁的数据。客户端 1 以为自己拿着锁但实际上锁在新的主节点上不存在客户端 2 也能加锁成功互斥被打破。这就出现了一种非常讽刺的场景你的锁从“不存在”变成了“不存在的存在”业务上的重复执行已经开始而系统表面上一切正常直到你发现订单多生成了一笔。3.3 哨兵与一致性配置的权衡哨兵的主要职责是监控主节点的存活状态主节点挂了就自动把从节点提升为新主节点。它解决了“Redis 不可用”的问题但解决不了“锁数据未同步”的问题。有人会建议配置min-slaves-to-write和min-slaves-max-lag让主节点在从节点没有跟上之前拒绝写入。这个方案在锁场景下其实很鸡肋它确实能在一定程度上保证“写入至少被多少从节点接收”但会引入新问题——锁的可用性会大幅降低只要有少量从节点延迟就加锁失败。我实际项目里的做法是把主从复制的“一致性”和分布式锁的“安全性”分开来看。分布式锁对一致性要求极高如果业务真的无法容忍锁丢失那就别指望主从节点直接上集群方案也就是后面说的 RedLock 思路。如果业务能容忍极小概率的锁丢失那主从 哨兵是性价比最高的方案。4. 集群锁 RedLock用多数共识对抗宕机Redis 官方并不建议用主从架构解决分布式锁的可靠性问题Antirez 提出了一个替代方案——RedLock也叫红锁。它的思路是不用单个 Redis 实例而是用多个相互独立的 Redis 节点只有拿到了大多数节点的锁才算真正加锁成功。4.1 RedLock 提出的背景与核心流程先弄清楚一个问题为什么“多个独立节点”能对抗宕机因为单节点宕机的概率是 p5 个节点同时宕机的概率约等于 p 的 5 次方。这不是简单地从 1 个节点变成 3 个节点做冗余而是把“必须所有节点都活着”变成“只要大多数活着就行”。RedLock 的加锁流程是这样的客户端获取当前系统时间记为 T1。依次向 N 个 Redis 节点发送加锁请求每个请求都带相同的 key、value 和一个超时时间。只要大多数节点至少 N/2 1 个加锁成功并且总耗时小于锁的过期时间就认为加锁成功。如果加锁失败比如只拿到了少数节点就向所有节点发起释放锁的请求。这里有两个很关键的细节每个节点上的锁过期时间必须一样这是为了防止某些节点上锁先过期、某些节点上锁还在造成状态不一致;加锁请求本身要设置较短的超时比如 50 毫秒不然某个节点网卡了会拖慢整体加锁速度。4.2 红锁的争议与工程上的妥协RedLock 并不是一个“完美无瑕”的方案业界对它有不少争议。Martin Kleppmann 写过一篇著名的文章《How to do distributed locking》对 RedLock 提出了两条批评。第一条是关于 GC 停顿的。假设客户端 1 已经拿到了 5 个节点里 3 个节点的锁但在执行临界区代码前JVM 发生了一次长 GC暂停了 30 秒。而锁的超时时间设置的恰好是 30 秒。等 JVM 恢复时锁已经过期了另外的客户端 2 成功加锁并进入临界区此时客户端 1 也恢复正常继续执行两个客户端同时在临界区运行。第二条是关于时钟跳跃的。如果某个 Redis 节点配置了 NTP 时钟同步系统时间突然向前跳了几秒这个节点上的锁可能提前过期。这些问题在理论上确实存在但在工程实践里RedLock 仍然是目前基于 Redis 的分布式锁方案里安全性相对最高的一个。如果你业务上真的不能容忍锁丢失并且不想引入 ZooKeeper 或 etcdRedLock 是最合理的选择。4.3 Redisson 的看门狗与自动续期在 Java 生态里Redisson 已经内置了 RedLock 的完整实现而且额外提供了“看门狗”机制这是实际项目里最有价值的能力之一。Redisson 默认的锁超时时间是 30 秒加锁成功后后台会启动一个定时任务每 10 秒检查一次锁是否还持有。只要锁还在就把过期时间重置为 30 秒相当于锁永远不会因为业务没执行完而过期。这个机制叫看门狗续期。我自己用 Redisson 时的配置大概是这样RLock lock redissonClient.getLock(lock:order:12345); boolean locked lock.tryLock(5, 30, TimeUnit.SECONDS); if (locked) { try { // 业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }tryLock的第二个参数是锁的超时时间传 30 秒意味着看门狗生效;如果你自己显式指定了超时时间比如 10 秒那看门狗就自动失效锁到 10 秒就强制过期。我踩过的一个坑是业务里嵌套调用最外层锁了 30 秒但内部一个远程调用耗时 20 秒外层看门狗一直续期看似没问题但中间如果发生 Redis 节点故障红锁还需要重新获取其他节点非常容易超时。这让我总结出一条经验锁的范围要尽量小嵌套锁能不用就不用。5. 从“锁不丢”到“锁安全”面试里更高级的追问视角这个话题走到这一步其实已经超越了“Redis 宕机锁会不会丢”本身而是上升到“分布式锁的语义边界到底在哪里”。面试官如果继续追问通常就是这几个方向锁超时、时钟跳跃、业务执行时间与锁过期时间的匹配、GC 停顿。5.1 时间和时钟跳跃锁安全的反面分布式锁的过期时间本质上是依赖系统时钟的。每次加锁都带一个 PX 过期毫秒数Redis 在键过期时执行删除。但系统时钟不是绝对可靠的尤其是在虚拟化环境下时钟漂移和 NTP 跳变都很常见。如果某个 Redis 节点的系统时钟往前跳了 10 秒这个节点上的锁就会提前过期。这时候即使你用的是 RedLock只要提前过期的节点数量超过了 N/21锁就会失效。反过来如果时钟向后跳锁的过期时间会被拉长相当于一个已经逻辑过期的锁仍“活”着。解决办法有两个层面运维上给 Redis 配置no-ntp或者关闭 NTP 自动同步在物理机或专用虚拟机上部署降低时钟漂移概率;代码层面尽量缩短锁的过期时间让时钟跳跃造成的窗口更小。5.2 业务超时与锁过期谁说了算这是一道经典的延伸题锁过期时间设置成多少合适太短业务没执行完锁就没了其他线程趁虚而入;太长一旦持有锁的节点挂掉其他节点要等很久才能抢到锁业务阻塞严重。没有完美的固定值正确做法是结合业务评估最慢执行时间然后乘一个安全系数。比如订单支付接口通常要求 5 秒内返回结果那锁的过期时间可以设置成 15 秒到 30 秒。如果你的业务里有长事务、消息队列轮询、外部接口调用这类不确定性高的操作要么把锁时间设置得很宽裕要么用 Redisson 的看门狗自动续期。这里最容易犯的错是只在加锁时设置过期时间而业务执行时间超过锁过期时间后没有任何补偿机制。面试官一旦问“业务还没执行完锁就过期了怎么办”就要答出看门狗续期、锁续期任务、或者在临界区开头校验锁状态这三层方案。5.3 主从切换、GC 停顿与运维冗余我把主从切换和 GC 停顿放在一起是因为它们都属于“分布式锁的元凶三件套”——Redis 侧故障、客户端侧停顿、时钟侧异常。主从切换的问题前面已经详细说过核心就是异步复制下新主节点可能没有锁的数据。GC 停顿的问题是客户端拿着锁JVM 停顿时间大于锁过期时间锁被自动释放另一个客户端拿到锁前一个客户端恢复后继续执行导致并发。这两种情况用 RedLock 都只能部分缓解没法完全消除。所以严格来说Redis 分布式锁的定位是“高可用场景下的工程方案”不是“严格安全性场景下的共识方案”。如果业务真的连毫秒级的锁丢失都无法容忍比如金融转账、库存扣减、秒杀超卖那别的选择是 ZooKeeper 或 etcd。ZooKeeper 基于 ZAB 协议和临时顺序节点实现锁节点宕机后锁自动消失;etcd 基于 Raft 协议写入必须大多数节点确认才能成功天然保证了强一致性。所以面试题如果追问“Redis 分布式锁和 ZooKeeper 分布式锁有什么本质区别”最核心的差异不在于“谁性能好”而在于“谁是强一致”。Redis 是 AP 模型保证可用性但可能丢数据;ZooKeeper 是 CP 模型保证一致性但在极端情况下会短暂不可用。分布式锁这种场景一致性比可用性更重要。6. 实践路上的坑与排障实录理论知识说完了讲点我在项目里实际遇到的问题。这些坑不是从文档里抄来的全是踩过之后总结出来的。6.1 一个典型的“锁丢失”事故复盘去年有个项目线上订单接口偶尔出现重复支付的回调。排查过程很曲折最终定位到主从切换引发的锁丢失。当时架构是 Redis 一主一从每天凌晨会做一次主从切换演练。切换发生在凌晨三点而凌晨正好有一批定时任务在跑。主节点切换后某个从节点被提升为主节点但同步延迟导致锁数据丢失两个定时任务同时进入同一个临界区重复推送了支付回调。当时我没有把锁的 key 打点记录下来导致排查时只能靠日志上下文推测。后来我在所有加锁、解锁的代码里都加了结构化日志记录 key、value、客户端 IP、耗时、超时时间这些维度再出问题就能按时间线快速定位。6.2 Redis 侧能做的排查与监控分布式锁出问题第一件事不是看业务代码而是看 Redis 侧的关键指标INFO stats里的expired_keys如果短时间内大量递增说明锁大量提前过期。INFO replication里的master_repl_offset和从节点的slave_repl_offset差值过大说明主从同步延迟高。INFO keyspace里的当前锁 key 数量变化如果出现断崖式下降大概率是宕机和重启。sentinel 日志里的failover事件节点切换时间点要和业务异常时间点对上。我见过一个团队把锁的 value 清一色用固定字符串不携带业务唯一标识结果出了问题连“是哪个请求加的锁”都查不出来。正确做法是 value 里带上业务 ID 或者请求 ID 的哈希排查问题时有据可查。6.3 面试时怎么把这个题答出层次感如果面试官问你“都说用 Redis 做分布式锁那如果 Redis 宕机了锁不就丢了吗”我的建议是不要一上来就答 RedLock而是分层递进。第一层承认问题。单机 Redis 宕机锁确实会丢因为内存数据本身就不持久这是 Redis 分布式锁的老问题。第二层给出工程兜底。可以通过 AOF 持久化 主从 哨兵把单点故障变成高可用把丢失概率降到最低。第三层指出隐藏问题。持久化只能恢复重启后的数据但主从切换时异步复制会丢锁这才是高可用下真正的坑。第四层主打集群方案。用 RedLock 或 Redisson 的 MultiLock在多个独立节点上获得大多数节点的同意把单点故障的影响摊薄同时引入看门狗自动续期解决业务超时和锁过期的矛盾。第五层强调语义边界。如果有极严格的强一致要求需要评估 Zookeeper/etcd 方案并和面试官讨论业务场景对一致性与可用性的权衡。这几层打下来面试官至少能确认你不仅背过答案还真把这个问题的脉络梳理清楚了。最后再分享一个我在项目里的小经验不管用哪种方案分布式锁的 key 和 value 都要有清晰的命名规范和可观测性把锁信息打到日志、监控、调用链任何一个端到端平台里。分布式锁最怕的不是出问题而是出问题后你们不知道问题出在哪一环。把每把锁的加锁时间、持有节点、释放时间都可视化出来排查效率能提升一大截。