面试官问出“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,再删除 key:
if 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 提出的背景与核心流程
先弄清楚一个问题:为什么“多个独立节点”能对抗宕机?因为单节点宕机的概率是 p,5 个节点同时宕机的概率约等于 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 或 etcd,RedLock 是最合理的选择。
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/2+1,锁就会失效。反过来,如果时钟向后跳,锁的过期时间会被拉长,相当于一个已经逻辑过期的锁仍“活”着。
解决办法有两个层面:运维上,给 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 都要有清晰的命名规范和可观测性,把锁信息打到日志、监控、调用链任何一个端到端平台里。分布式锁最怕的不是出问题,而是出问题后你们不知道问题出在哪一环。把每把锁的加锁时间、持有节点、释放时间都可视化出来,排查效率能提升一大截。