
1. 从能用到高可用我在分布式锁这条路上踩过的坑聊到Redis分布式锁大部分人第一反应就是SETNX加EXPIRE或者直接甩出Redisson的几行代码。但真正从单机Redis一路走到集群环境你会发现这套看似简单的机制里全是暗礁。我最早在项目里用Redis做分布式锁就是图它快、接入简单结果线上出了几次诡异的并发问题排查了很久才发现不是业务代码的问题而是锁本身在最基础的实现上就不严谨。这篇文章想做的不是重复网上那些Redis分布式锁原理的科普而是把我从单机到集群环境下分布式锁方案一步步演进的过程完整复盘一遍。你会看到为什么单机锁在多线程场景下也能翻车为什么主从切换会把锁直接弄丢为什么RedLock在业界争议那么大以及最终在生产环境里我选择用什么样的组合方案来保证锁的可用性和安全性。如果你正在负责一个需要分布式锁的中小型项目或者准备面试被问Redis分布式锁怎么实现才可靠这篇内容应该能帮你省下不少摸索时间。我会尽量把每个阶段的坑、底层原因和验证手段都讲透代码和配置也都是可以直接落地参考的。2. 单机Redis时代的锁SETNX的甜头与致命短板2.1 第一版实现SETNX加EXPIRE为什么不够最早接触到分布式锁绝大多数资料给的示范都是这样的import redis r redis.Redis(host127.0.0.1, port6379) def acquire_lock(lock_key, request_id, expire_seconds): # SETNX: key不存在时才设置成功 result r.set(lock_key, request_id, nxTrue, exexpire_seconds) return result def release_lock(lock_key, request_id): # 只有持有锁的人才能释放用Lua保证原子性 script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end return r.eval(script, 1, lock_key, request_id)这套写法相比老早之前那种SETNX成功后再单独EXPIRE已经强多了因为SET key value NX EX seconds是原子操作不会出现在拿到锁之后还没来得及设置过期时间就宕机导致的死锁问题。我第一次在生产环境用这套代码的时候确实也稳定跑了一段时间。当时业务场景是控制定时任务在多个应用实例之间不能重复执行单机Redis、三台应用节点每天跑几次看起来一切正常。但后来压测的时候问题冒出来了高并发下同一个业务请求的持有锁时长如果波动很大就会出现锁被提前释放、或者任务还没执行完锁就过期的情况。这里的关键是过期时间设多少。设短了任务执行超过这个时间锁没了其他节点趁虚而入设长了一旦持有锁的节点真的挂掉其他节点要等很久才能拿到锁。第一个版本我拍脑袋设了个30秒结果有个批处理任务偶发执行到40秒直接导致两个节点同时跑同一批数据。2.2 持有锁时长和过期时间的博弈后来我做了个简单的对比表梳理出不同取值下的影响过期时间设置优点缺点适用场景5秒节点故障后快速恢复可用正常慢任务容易锁过期所有任务都在几秒内完成30秒适配大多数业务故障恢复慢慢任务仍需评估大多数中小型任务60秒以上慢任务友好故障恢复极慢锁泄露风险大任务时长不稳定且频率低我当时选30秒的依据是看历史任务耗时P99但忽略了一个问题P99是过去的数据不代表未来的某一次请求不会因为网络抖动、GC停顿、外部接口变慢而突然膨胀到远超P99的耗时。这个问题的根治方案不是把过期时间不断调大而是引入续期机制——锁持有者定期检查自己的任务是否还在执行如果还在执行就主动把过期时间往后推。那时候项目里已经有Redisson的依赖了我意识到与其自己维护续期逻辑不如直接看看Redisson是怎么设计的这也是后面我切换到看门狗机制的直接原因。2.3 单机锁最容易被忽视的问题锁的公平性与重入性单机Redis锁还有一个被面试题反复问、但实战里经常被忽略的点Redis分布式锁天然不是公平锁。多个请求同时抢锁谁能抢到完全取决于网络到达顺序和Redis处理顺序不存在先来先得的概念。在一些对顺序有要求的场景里比如库存扣减、订单状态流转非公平锁本身问题不大因为业务逻辑可以用乐观锁兜底但在某些消息推送、资源分配场景里非公平性会导致部分请求持续饥饿。另外就是可重入性。同一线程在持有锁期间如果递归调用加锁逻辑或者在一个方法里先加锁再调用另一个同样加锁的方法简单SETNX会直接被自己挡住。Redisson的锁是基于哈希结构实现的同一个线程可以重复加锁内部维护了加锁次数计数释放锁时递减到0才真正删除。如果只是自己玩玩拿SETNX凑合没问题但想设计一个能被团队成员安全复用的锁组件重入性是从第一天就该考虑的。我在第一阶段的体会是单机Redis锁适合在业务逻辑足够简单、任务执行时长可控、Redis本身不会挂的场景里使用。一旦并发模型变复杂它就开始漏风了。3. 从单机到主从锁是如何在故障切换瞬间悄悄丢掉的3.1 搭建Redis主从复制后的新问题随着业务量上来单机Redis扛不住了我们把Redis从单机升级成了主从架构一主一从加哨兵主打高可用。这套架构对缓存数据来说非常友好主节点挂了哨兵会提升从节点为新的主节点应用层几乎无感知。但分布式锁在这套架构下出了一个非常隐蔽的问题。设想这样一个时序客户端A往主节点写入锁SETNX成功拿到了锁。主节点在把这条数据同步给从节点之前忽然宕机。哨兵检测到主节点不可用将从节点提升为新的主节点。客户端B此时来抢同一把锁发现新的主节点上根本没有这条锁记录SETNX成功。结果就是客户端A和客户端B同时认为自己持有了锁分布式锁形同虚设。当时线上出现这个问题的时候最麻烦的点在于现象不是每次都发生。只有恰好卡在写入锁成功但主从尚未完成同步这个极窄的时间窗口里才会触发。我们最初怀疑是业务逻辑有问题反复查了很久才往Redis主从同步延迟这个方向排查。3.2 为什么哨兵模式修复不了这个缺陷哨兵的作用只是检测主节点故障并完成切换它完全不理解锁数据是否已经同步到了从节点。这个缺陷的出现不是哨兵的配置问题而是数据复制模型本身就存在异步窗口。我们来看一下主从同步的基本流程客户端 - 主节点SETNX成功 主节点 - 返回成功给客户端 主节点 - 异步将写命令发送给从节点 从节点 - 执行命令完成数据同步关键就在第三步和第二步之间。主节点给客户端回包之后数据在从节点上可能根本还没落地。任何时刻主节点宕机这段没同步到的数据就永久丢失了。有些人会想那我改成同步复制不就行了主节点写成功后必须等从节点确认才返回成功这样锁就不会丢了吧理论上确实是但同步复制的代价是每一次加锁都要等待网络RTT和从节点fsync性能直接下降一个量级。而且如果从节点暂时不可用主节点会被拖死这在Redis的高可用设计中是不可接受的。所以Redis官方默认用了异步复制这也就意味着只要使用Redis标准主从架构分布式锁就一定存在这个丢失窗口。3.3 我们当时的排查过程从怀疑业务到定位主从延迟我把当时的排查链路写下来如果你正好也遇到类似问题可以对照着走一遍。第一步是看日志。当时我们发现并发冲突的业务场景集中在凌晨的定时任务上多台应用服务器都会执行同一个任务但任务本身做了幂等控制只有偶尔日志里会出现重复执行的告警。第二步是加锁日志和业务日志做时间对齐。我们把每次加锁成功、释放锁的时间戳以及业务实际执行的时间戳全部打出来对比后发现一个规律重复执行发生的时间点都紧跟在Redis主节点一次故障切换之后。第三步是查看Redis的复制状态。通过INFO replication命令我们看到了主从节点的复制偏移量差距确认了在故障发生之前主从之间的数据并不是实时一致的。第四步是验证锁是否真的丢失。我们在测试环境人为制造了一次主从切换复现了客户端B获取锁成功的情况这才完全确认问题根源。这个排查过程给我最大的教训是分布式环境下出现问题不要第一时间怀疑代码逻辑先检查基础组件的复制、同步和切换机制。很多时候答案就在你平时不太注意的配置项里。4. 集群模式下的新变量slot路由与多节点写入的复杂度4.1 Redis Cluster不是简单的主从放大版从主从架构升级到Redis Cluster之后第一个直观感受是锁的写入目标节点变得不确定了。Redis Cluster把数据分成16384个slot根据key的CRC16值决定存在哪个节点上。同一个锁key在集群模式下会固定落到某一个slot也就是固定落到某一个节点。单看这一点锁本身的存储位置是确定的问题不大。但集群模式引入了一个新变量如果包含锁key的那个master节点宕机了slot会迁移到从节点或者failover到其他节点。这个过程的实现细节直接决定了锁在故障期间的可用性。在Cluster模式下客户端访问key的时候如果正好遇到slot迁移会收到MOVED错误或ASK错误。对于分布式锁来说这意味着加锁请求可能被重定向到新节点旧节点上的锁数据可能还没被清理也可能已经丢失。这比主从架构的单点切换又复杂了一截。4.2 为什么Cluster下更依赖Redisson之类的高级客户端我个人的体会是到了集群阶段自己用原生SETNX拼分布式锁的维护成本开始指数上升。你不仅要处理加锁、释放、过期还要处理slot重定向、节点故障感知、锁的跨节点一致性这些问题单独拎出来每一个都能写一篇排查文档。与其自己重复造轮子不如使用成熟的客户端库。Redisson在集群模式下会处理slot路由它的RLock接口在底层使用的是hash结构加锁时会通过Lua脚本同时检查锁的存在性、线程标识、重入计数并且自动适配集群模式。我在项目中切换到Redisson之后最大的变化是代码量减少但并不意味着可以无脑使用。Redisson的锁在集群模式下同样存在争议这正是下一节要展开的RedLock话题。4.3 集群锁的现实场景MGET与多key事务的伪命题还有一个经常被搞混的细节很多人以为Redis集群模式下分布式锁变得不靠谱是因为多key操作受限。比如Lua脚本里同时操作多个key在Cluster模式下要求所有key必须在同一个slot否则会报CROSSSLOT错误。但实际上分布式锁的加锁逻辑通常只关心一个锁key不涉及多个key的跨slot问题。以Redisson为例锁的key、加锁计数、线程标识都在同一个Redis hash结构里天然满足单slot要求。因此集群模式并不会让单把锁的使用变得复杂复杂的是当你的业务逻辑需要同时加多把锁、并且希望它们具有原子性时你才会感受到Cluster的约束。比如一个转账操作要锁A账户和B账户这两个key可能分布在不同的slot上跨节点的事务没法保证只能通过其他手段协调。这条路径走下去就已经不是Redis分布式锁能解决的范畴了。5. RedLock算法与它的争议大师方案为什么没有成为银弹5.1 RedLock的核心思路Redis作者Antirez提出了RedLock算法专门解决主从异步复制下锁丢失的问题。核心思路很简单不把鸡蛋放在一个篮子里。假设有N个完全独立的Redis节点比如N5客户端加锁时按顺序向这5个节点都尝试加锁。只有满足以下条件才认为加锁成功在大多数节点至少3个上成功加锁。加锁总耗时小于锁的过期时间。从开始加锁到最终确认锁的剩余有效时间仍大于业务执行所需的最小时间。释放锁时向所有节点发起释放命令即使某些节点上的锁已经过期也没关系。这个算法的本质是单点故障被多数节点冗余抵消了。任何一个节点的数据丢失只要不超过半数节点同时丢失整个锁系统依然可靠。5.2 我在测试环境尝试RedLock时的实现细节网上关于RedLock的讨论很多但大多数停留在理论层面。我实际在测试环境实现过一个简化版本用来验证它在故障注入下的表现。选5个独立的Redis实例不互相复制数据各自独立运行。客户端加锁时依次对5个实例执行SET NX统计成功数量。class RedLock: def __init__(self, redis_nodes, quorum3): self.nodes redis_nodes self.quorum quorum def acquire(self, lock_key, request_id, ttl): success_count 0 start time.time() for node in self.nodes: try: if node.set(lock_key, request_id, nxTrue, exttl): success_count 1 except Exception: # 单节点异常不影响整体 continue cost time.time() - start if success_count self.quorum and cost ttl: return True # 没拿到足够票数就释放已获取的锁 self.release(lock_key, request_id) return False def release(self, lock_key, request_id): for node in self.nodes: try: node.delete(lock_key, request_id) except Exception: continue这段代码离生产级RedLock还差得远比如没有处理时钟漂移、没有在网络分区场景下做验证但用来理解算法本身是够了。测试结果让我印象很深在随机杀掉2个节点的情况下RedLock确实还能正常加锁和释放锁没有丢。但一旦出现网络分区问题就开始显现。5.3 为什么RedLock在业界争议这么大RedLock最大的争议点在于它依赖一个隐含假设各节点的时钟是基本同步的。但现实世界里服务器时钟可能因为NTP调整、时钟漂移而出现跳跃。假设客户端A在节点1上加锁成功此时节点1的时钟突然往前跳了一大段锁提前过期客户端B就能加锁成功两个客户端同时持锁。另一种争议场景是客户端A加锁成功后在释放锁之前发生了长时间的GC停顿Stop-The-World。等GC结束恢复执行时锁已经在所有节点上过期了。此时客户端B加锁成功开始执行客户端A却还在继续自己的业务逻辑完全不知道自己已经失去了锁的合法性。这两种情况RedLock都无法彻底解决因为它们本质上是分布式系统中时间与暂停的问题不是靠选举多数节点就能绕过去的。还有一个更现实的批评是RedLock要求5个独立节点成本和运维复杂度直线上升对于大多数中小团队来说只是为了一个分布式锁而维护5个Redis实例性价比存疑。5.4 我的结论RedLock在什么场景下值得用我的最终判断是如果你们的分布式锁用在内部服务之间的资源争抢业务可以容忍极端情况下的偶发并发那么普通的单实例Redis加锁配合Redisson的看门狗续期已经足够。如果你们的锁事件影响资金、订单、库存等强一致业务那根本不应该把宝押在Redis上建议直接考虑etcd或ZooKeeper这类带强一致性协议的组件。RedLock并不能把Redis变成强一致系统。换句话说RedLock适合的场景其实非常窄业务上需要Redis的高性能又希望比单点方案在故障率上更低一档且能接受RedLock在理论上的那些边缘风险。至少我目前没有在哪个线上系统里看到RedLock被正确且严格地落地过。6. 最终落地Redisson加看门狗以及我为什么放弃了纯手写6.1 Redisson的核心机制看门狗续期在经历了手写SETNX、主从复制锁丢失、研究RedLock之后我最终在生产环境选择了Redisson作为分布式锁的基础库。选它的核心原因不是因为它大师出品而是它把分布式锁工程化需要解决的那些细节问题都收敛了。最有名的是看门狗机制。默认情况下Redisson加锁时如果没指定leaseTime会启用一个后台定时任务每隔锁剩余有效时间的三分之一就自动为还在持有的锁续期。比如默认锁超时时间是30秒看门狗每10秒检查一次如果锁仍然被当前线程持有就把过期时间重新延长到30秒。这套机制直接解决了我在单机阶段手动设置过期时间的两难困境不用再担心任务慢导致锁提前过期也不用担心节点挂了锁永远不释放。节点挂了锁自然过期任务还在执行锁会持续续期。只有一种情况会出问题持有锁的线程发生长时间GC停顿看门狗线程也被冻住了等恢复时锁已经过期。这个问题Redisson也无法完全避免但可以通过告警和业务幂等兜底。6.2 Redisson在单机、主从、集群三种模式下的表现对比我整理了在实际使用中的对比方便你根据自身架构选择部署模式Redisson是否支持锁的可用性风险推荐指数单机Redis支持节点宕机即锁服务不可用开发测试可用主从哨兵支持主从切换瞬间锁可能丢失低峰期可用Redis Cluster支持故障转移时可能短暂不可用生产主力方案多独立节点RedLock需自定义理论降低丢失风险但复杂度高特殊场景Redisson对Redis Cluster的原生支持是我最终落地的最大推手。它内部实现了slot感知加锁的key无论路由到哪个节点都能正确处理同时看门狗的续期逻辑也不会因为节点切换而中断。6.3 生产环境配置建议与避坑清单如果你决定用Redisson下面这些配置点是我压测和生产环境验证过的建议锁超时时间方面优先使用默认的30秒加看门狗。如果业务不允许默认值明确指定leaseTime时一定要把值设成理论上任务最大耗时的1.5到2倍给GC和网络抖动留冗余。等待锁的最长时间waitTime也就是尝试获取锁的超时时间默认值是-1表示无限等待。生产环境务必改成有限值否则一旦Redis节点异常大量线程会阻塞在waitingLock上拖垮应用线程池。锁的底层数据结构和公平性Redisson默认使用的是非公平锁如果你需要公平锁可以使用RedissonFairLock但它基于ZSet实现性能会比默认差一些适用于低频强顺序场景。与Spring AOP的集成用RLock注解可以省掉手动加锁释放的样板代码但要注意锁的粒度不要在一把全局锁上挂太多业务逻辑锁的粒度越小系统吞吐量越高。我在实际项目中最终形成的规范是这样的1. 所有分布式锁统一通过Redisson的RLock接口操作。 2. 锁的key命名统一为:lock:业务领域:资源ID。 3. 锁内只做核心临界区操作不做远程IO和重计算。 4. 释放锁放在finally中避免异常路径导致锁泄漏。 5. 对锁的获取失败做降级处理优先走本地限流或快速失败。这套规范帮助团队在很长一段时间里没有再出现锁相关的线上事故。6.4 为什么我不再手写分布式锁手写SETNX锁并不是不行它最大的问题是看着简单做对很难。你至少需要同时处理过期时间的设置、持有者标识、释放时的原子性校验、可重入计数、续期机制、异常时的锁清理、Redis节点切换的适配。这些需求堆在一起自己维护的成本早就超过引入一个成熟库的成本。Redisson不是没有缺点代码量较大、依赖较重、需要理解其内部机制才能用好但比起在凌晨三点被一个锁丢失的告警叫醒去翻日志这些成本我觉得完全值得。6.5 关于时钟跳跃与GC停顿的最后一点经验最后我想说一个容易被忽略的点。无论你用哪种Redis分布式锁方案锁的保证都是有时限的。Redis锁本质上是一种带有租约的互斥机制它依赖锁过期时间来兜底异常场景。因此业务层必须做到拿到锁后执行的任务必须具备幂等性即使重复执行也不会产生严重错误。任务核心操作最好有独立的事务控制或版本号校验不能假设有了锁就可以放弃所有并发控制。对GC停顿、时钟跳跃这类极端场景应该通过监控告警来发现而不是指望锁算法做无限补偿。我踩过的最深一坑是团队小伙伴把分布式锁当成了只要拿到锁就绝对安全的万能药结果在一次Full GC导致锁过期后两个节点同时执行了导数据任务产生了重复数据。事后清洗数据花了两天时间。从那以后我在团队里反复强调一个观点Redis分布式锁解决的是绝大多数场景下的互斥而业务幂等性才是最后一道安全网。这两个东西配合好系统才能真的稳。