
先说结论Redis锁机制不是一个炫技玩具它解决的是“多个进程/多台机器之间如何保证同一时刻只有一个节点能做某件事”这个分布式系统里的头号难题。如果你已经在用Redis那么基于它实现分布式锁是性价比最高的方案没有之一。这篇文章我会从“它到底牛在哪儿”和“平时能帮咱们解决啥大问题”两条线展开结合我这些年实际踩过的坑把原理、选型、代码实现和排障经验一次说透。1. 锁机制核心价值为什么单机锁在分布式环境里不灵了1.1 单机锁的天然局限很多初学者会觉得锁嘛不就是synchronized或者Lock那点事。在单机单进程场景下确实如此JVM 内置锁在进程内部就能保证互斥。但现实情况是稍微有点规模的系统尤其是面向C端的业务服务基本都是多实例部署的前面还挂着负载均衡。你写一个定时任务在单机模式下是单实例执行的没问题一旦部署了两台、三台机器每个实例都会在同一时刻触发任务如果不加控制数据库就会被重复处理的数据搞乱。这时候单机锁完全失效因为每台机器上的 JVM 是独立的A 机器的锁 B 机器根本感知不到。你需要把“锁”这个概念从进程内存提升到一个所有机器都能访问的公共区域而这个公共区域通常就是数据库、ZooKeeper或者今天我们聊的主角 Redis。1.2 为什么偏偏是Redis而不是数据库或ZooKeeper数据库行锁或悲观锁也能做互斥比如SELECT ... FOR UPDATE但性能瓶颈很明显。每次获取锁都要走一遍SQL、事务、行锁竞争高并发时段数据库连接很快就满了另外还会引入长事务、死锁检测这些额外负担。ZooKeeper 的临时顺序节点做锁也很可靠但部署和运维成本摆在那里一个ZK集群要占不少资源操作起来也要引入额外的客户端依赖。Redis 的杀手锏就两点快、简单。纯内存操作单次SET命令耗时通常在亚毫秒级高并发下能扛得住每秒几万甚至十几万的锁请求。而且绝大多数公司本来就有现成的Redis集群不需要额外维护一套中间件直接用就行。这也是分布式锁场景里Redis成为默认首选的原因。不过先泼一盆冷水Redis分布式锁不是银弹它是有适用边界的在极端场景下存在理论上的瑕疵。后面我在常见问题部分会专门展开。先把它“牛”在哪里讲清楚再告诉你怎么用才不会翻车。2. Redis锁的底层原理SET NX EX为什么能扛起互斥大旗2.1 核心命令的前世今生Redis 分布式锁最核心的底层逻辑就浓缩在一条SET命令里面。它的关键参数是NX和EX或者PX。NX的意思是Not eXists只有键不存在时才设置成功EX则是指定过期时间。两者结合的效果是多个客户端同时执行同一条SET key value NX EX 10Redis 内部是单线程串行处理命令的所以最终只有一个客户端能设置成功其他客户端全部返回nil。设置成功的那个人获得了锁其他没抢到的就进入等待或者快速失败重试。这条命令为什么说是历史性的进化因为在 Redis 2.6.12 版本之前实现分布式锁需要先SETNX再单独调EXPIRE设置过期时间。这两个操作不是原子的如果在SETNX成功之后、EXPIRE执行之前进程崩溃了锁就永远不会释放其他客户端只能干瞪眼这就是著名的“死锁隐患”。后来官方支持了原子性操作一条命令解决“加锁设置过期时间”从根上堵住了这个坑。所以现在写代码务必使用SET key value NX EX这种原子命令不要再拆成两步走。用生活化的例子理解NX就像酒店前台只有一把房卡你递上身份证问“这房卡有人拿了吗没人拿我就拿走”前台查了下没人拿就把卡给你了。EX就是你领卡时酒店规定的强制退房时间不管你愿不愿意到点房卡自动失效防止有人拿了卡失踪。2.2 Value的设计隐藏的大学问很多人会忽略锁的value该存什么。如果只是随便存个固定字符串1后续释放锁的时候就会出问题。我见过不少团队踩过这个坑A 客户端加的锁因为业务执行时间太长锁自动过期了后来 B 客户端成功加锁此时 A 业务执行完了跑来DEL锁结果把 B 的锁给删掉了。B 还在执行任务呢锁就没了别的客户端又能加锁互斥就失效了数据就乱了。解决这个问题的标准做法是value 存一个全局唯一的随机字符串比如UUID、雪花ID或者ThreadLocalRandom生成的长随机数。释放锁的时候不能单纯DEL得用 Lua 脚本先比对 value匹配上了才删除不匹配就放弃删除。这就是所谓的“只有锁的持有者才能释放锁”。if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段 Lua 脚本是分布式锁释放环节的通用标准。对比和删除在 Redis 服务端原子执行保证判断和删除之间不会被插入其他命令。这个细节做对了锁才算真正安全。2.3 过期时间怎么定拍脑袋定数值最容易出事过期时间设置多长直接决定系统在各种异常场景下的表现。设太短业务还没执行完锁就过期了其他线程趁虚而入造成并发冲突设太长如果持有锁的节点真挂了锁要等很久才能被回收阻塞后面大量请求。我遇到过一个经典场景一个定时任务同步订单数据平时几百毫秒跑完锁过期时间设置了 10 秒正常情况下很稳。结果双十一大促数据量暴涨单个任务跑到 15 秒锁在第 10 秒就过期了另一个实例又获取了锁两个实例同时跑同步任务同一条订单被同时处理了两遍产生了大量重复数据。所以过期时间的设置是有讲究的先压测或者统计业务方法耗时的 P9999%请求耗时在这个基础上乘一个安全系数。比如 P99 是 3 秒可以设置 10 秒左右给足余量。但业务耗时如果波动极大固定过期时间就不够用了需要引入“看门狗续期”机制也就是持有锁期间自动续期确保锁不过期。主流的 Redisson 框架里这个功能是内置的默认锁续期时间 30 秒每过 10 秒检查一次如果业务还在执行就重新设置过期时间为 30 秒。这也是我强烈建议直接用 Redisson 而不是自己造轮子的原因之一。3. 业界主流Redis锁选型对比手写、Redisson与红锁3.1 手写方案为什么只适合学习和简单场景网上能搜到大量手写 Redis 分布式锁的博客代码通常就几十行核心就是SET NX EX加 Lua 释放锁。这种方案的学习价值很大让我能彻底吃透 Redis 锁的底层运作但生产环境我一般不推荐直接拿去用。原因很简单它只解决了“加锁、释放、过期”三个最基础的问题但分布式锁在生产环境需要的能力远不止这三样。比如可重入性。一个线程已经持有锁了在锁保护的方法里又调用另一个同样需要这把锁的方法如果锁不支持重入就会导致自己锁自己出现死锁。手写方案要实现可重入还得额外维护计数器这些数据结构复杂度就上来了。再比如续期问题上面提到的看门狗机制手写方案也要自己实现后台守护线程去续期写起来容易出各种并发Bug。所以我的建议是手写方案用来理解原理、应付面试足够了但生产环境请直接用 Redisson 或者类似的成熟客户端。3.2 Redisson的看门狗机制和RLock使用Redisson 是 Java 生态里最成熟的 Redis 分布式锁客户端。它的核心能力是封装了可重入锁RLock用起来和 JVM 的Lock接口差不多上手成本极低。最简单的使用方式RLock lock redissonClient.getLock(order::pay:: orderId); try { // 尝试加锁最多等待3秒锁自动过期时间30秒 if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }Redisson 内部会在加锁成功之后启动一个定时任务也就是看门狗。默认锁的租期是 30 秒看门狗每 10 秒去 Redis 检查一次如果锁还在持有状态就把过期时间重置为 30 秒。这个机制有效避免了业务长耗时导致锁提前失效的问题。这里有个重要的细节tryLock方法如果释放了leaseTime参数也就是不传第三个参数才会启用看门狗如果你手动指定了 leaseTime那就不会续期了到期锁自动释放。所以用 Redisson 的时候要想清楚是让看门狗帮你兜底还是自己承担业务超时锁过期的风险。我习惯的做法是不指定 leaseTime让看门狗托管然后依靠业务兜底逻辑兜住重复执行的幂等性双保险。3.3 关于RedLock红锁的争议与现实选择RedLock 是 Redis 官方提出的一种多节点锁算法核心思想是在 N 个独立的 Redis 节点上同时加锁只要加锁成功的节点数超过半数就认为加锁成功。设计目的是解决单点故障问题——一个 Redis 节点挂了其他节点还能支撑锁的裁决。但 RedLock 在业界的评价一直存在巨大争议尤其在分布式系统领域有几位泰斗级人物专门写文章论证 RedLock 在异步网络模型下依然存在安全性漏洞。比如它无法处理“节点时钟跳跃”导致的问题或者某个节点在加锁成功后、返回给客户端之前发生了 GC 停顿锁其实已经过期但这些复杂状态无法被感知。我的实际建议是绝大多数公司的业务场景主从 Redis Redisson 的普通锁已经够用不需要上 RedLock。为什么因为你对锁的安全性要求取决于数据一致性风险的可承受度。如果丢失一次互斥会造成超卖、重复支付、库存为负这种全局性的资损问题单纯靠 Redis 锁还不够还得配合数据库唯一索引、状态机等兜底方案。如果只是一般的防重入、防并发执行普通 Redis 锁完全没问题。RedLock 的复杂度高运维成本大收益在绝大多数业务里体现不出来。4. 实战场景拆解Redis锁解决过的四个大问题4.1 场景一秒杀扣库存锁是防超卖的第一道防线秒杀是分布式锁最经典的应用场景。用户点击抢购后端要执行“查库存-判断是否充足-扣减库存-生成订单”的流程。如果不加保护高并发下多个请求同时读到库存还有 1同时执行扣减库存就变成了 -5这就是超卖事故。用 Redis 锁把整个扣减流程锁住每个请求拿到锁才能操作库存就能保证任意时刻只有一个请求在扣减。锁的粒度要精细到 SKU 级别比如锁的 key 设计为stock::skuId:10001而不是用一把全局锁锁住所有商品的库存否则一个商品被抢其他所有商品的请求全部排队性能直接崩盘。我这里补充一个经验库存扣减这种高频小操作其实不一定需要分布式锁用 Redis 的DECR命令配合返回值判断就能实现原子扣减性能比锁要高一个量级。但秒杀场景往往还需要校验用户是否已经参与过活动、记录订单信息等联动操作这些需要更复杂的业务原子性这时锁就派上用场了。4.2 场景二分布式定时任务防重复执行这个场景是我在项目里处理最多的。公司的定时任务系统通常基于分布式部署比如每隔 5 分钟同步一次广告数据、每隔 1 小时结算一次分成。如果没有锁集群里每个节点都会执行一次任务重复结算、重复推送消息就接踵而至。分布式定时任务防重复执行的锁用法很直接任务入口处先尝试加锁key 可以按任务名或数据批次来设计比如job::settlement::20250120。加锁成功说明没有其他节点在执行执行完毕后释放加锁失败说明已经有节点在跑了当前节点直接跳过。个人经验定时任务的锁过期时间一定比任务最长执行时间要长宁可给个相对大点的数值比如任务跑 10 秒就设 60 秒锁因为定时任务场景出现短时间锁失效的后果很严重重复执行造成的脏数据排查成本远高于多等一会儿锁。4.3 场景三幂等接口防重锁如何配合(负载均衡超时重试)微服务架构下接口幂等性是个老生常谈的话题。客户端发起的请求因为网络超时等原因重试消息队列消费时因为消费者挂了重投订单支付回调可能被第三方系统投递多次。这些重复请求必须在服务端被识别并拦截。Redis 锁配合唯一请求ID就能实现幂等控制。客户端每次请求携带一个全局唯一ID服务端先尝试SET keyrequestId NX EX指定秒数如果设置成功说明这个请求第一次到达就正常执行业务如果设置失败说明相同请求ID已经处理过直接返回之前的处理结果或者什么都不做。这个场景还有另一个高效写法用 Redis 的SETNX结合固定的幂等key存储结果值比如支付回调的payment::callback::orderNo第一次处理时写入处理结果后续重复回调直接读取结果返回。这两种方式我都用过核心都是利用 Redis 的原子性去重机制但要注意过期时间的设置——太短会在业务高峰时漏判重复太长则白白浪费Redis内存。4.4 场景四分布式全局ID或访问频率控制分布式锁也能用在一些资源协调场景比如多实例环境下生成某一类业务的递增序列号或者做接口级别的访问频控。虽然专门的组件解决更优雅但临时需求或轻量场景Redis 锁加INCR就能快速顶上去。比如做登录接口的防刷用户手机号维度限制每分钟最多发送 5 条验证码。用 Redis 记录用户发码次数INCR后判断是否超过阈值超过就拒绝。INCR本身是原子的在单次请求内不需要锁但如果限流逻辑是“先读次数-判断-再写次数”这种复合操作就需要 Redis 锁或者 Lua 脚本保证原子性。很多人学了分布式锁恨不得所有场景都用锁这是误区。能用原子命令解决的就不需要锁锁的代价是并发性能下降能省则省。5. 手把手实操从零构建一个够用的Redis分布式锁5.1 依赖准备和基础配置如果你走 Redisson 路线依赖引入非常简单。以 Maven 为例引入org.redisson:redisson-spring-boot-starter然后在配置里写上 Redis 连接信息Redisson 的RedissonClient就会自动装配好。这里有个小坑版本和 Redis 服务器的兼容性老版本 Redisson 对 Redis 7.x 的支持可能不如新版建议用最新稳定版本。如果你暂时不想引入重量级框架只想用原生 Redis 客户端写一个简易自用版本也不是不可以但至少要把这四件事做对原子加锁、超时释放、安全释放、容错重试。我早期的一个项目里就是用 Spring Boot 的RedisTemplate手写了一套简版锁框架代码如下public boolean tryLock(String key, String requestId, long expireSeconds) { // 原子操作key不存在才设置并设置过期时间 return redisTemplate.opsForValue() .setIfAbsent(key, requestId, Duration.ofSeconds(expireSeconds)); } public boolean unlock(String key, String requestId) { // Lua脚本比对value一致才删除 String script if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); Long result redisTemplate.execute(redisScript, Collections.singletonList(key), requestId); return result ! null result 0; }setIfAbsent方法在底层就是SET NX EX它返回true说明加锁成功。释放锁时通过 Lua 保证原子性。这套代码应付一般的并发互斥没有问题但不具备自动续期和可重入能力生产环境用之前要评估清楚。5.2 一把锁的最佳实操上下文实测中我发现即便用成熟框架代码写法不对照样出问题。给大家一个标准的业务锁使用模板可以当作团队规范来用String lockKey order::cancel:: orderId; RLock lock redissonClient.getLock(lockKey); boolean locked false; try { // 最多等3秒抢不到就放弃 locked lock.tryLock(3, TimeUnit.SECONDS); if (!locked) { // 记录日志提示用户稍后重试 return; } // 执行真正的业务操作 doCancelOrder(orderId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 处理中断逻辑 } finally { if (locked) { lock.unlock(); } }这里有几个细节值得展开。tryLock的等待时间并不是越大越好设大了一旦锁竞争激烈大量线程阻塞在等待锁上线程池容易被占满。设小了稍微一冲突就放弃又影响用户体验。常规操作是 2 到 5 秒具体要看业务耗时。还有一个细节是finally里释放锁这是铁律凡是锁操作必须保证最后能释放最稳妥的就是写在finally中。还有一个很多人会忽略的问题是lock.unlock()如果当前线程不是锁的持有者会抛异常。我在剪贴过 Redisson 的isHeldByCurrentThread()判断再释放避免异常这个习惯是从线上故障学来的。5.3 锁内操作耗时与优雅上线的权衡锁保护的业务代码原则上要“短平快”不要在锁内做耗时的远程服务调用、批量大数据查询。因为持锁期间其他需要同一把锁的请求都在等待锁持有时间越长系统吞吐量越低。我自己遇到过一次事故业务逻辑里要调用第三方开放平台的查询接口平时 50 毫秒返回那天第三方服务抖动接口超时 10 秒而我是带着锁调用的导致大量请求阻塞系统核心接口直接雪崩。从那以后我立了个规矩凡是在锁内的代码一律不能有外部RPC调用。如果业务逻辑实在绕不开外部调用那就把锁粒度缩小、只锁关键资源变更的部分或者考虑用乐观锁、版本号等机制替代。毕竟分布式锁的本质是牺牲部分并发度来换一致性你把一堆无关操作放进锁里就是无限放大了这种牺牲。5.4 锁粒度与性能压测数据参考写锁方案的时候锁的粒度设计直接决定性能。我在一个模拟的秒杀系统里做过一组压测数据1000 个并发请求锁粒度是“全局一把锁”QPS 大约只有 800 左右大量请求在排队平均响应时间 1.3 秒锁粒度细化到 SKU 维度后QPS 提升到了 3000 以上平均响应时间 300 毫秒以下。原因很好理解不同 SKU 之间的抢购互不影响能并行执行。另一个值得参考的粗糙基准在单条SET NX EX操作下单线程 Redis 实例每秒能执行约 10 万次指令分布式锁本身不会成为系统瓶颈。真正的瓶颈永远是业务代码里的数据库操作和外部调用。所以“Redis锁性能不够”通常是伪命题更大概率是锁的粒度没设计好或者锁内业务代码太重。6. 高频故障和隐蔽深坑这些坑我都替你踩过了6.1 锁自动过期导致并发穿透怎么从根上解决这是分布式锁最容易踩的坑也是面试必问的考点。场景刚才已经描述过业务执行时间超过了锁的过期时间锁自动释放其他客户端趁机加锁两个线程同时执行同一段业务数据一致性被破坏。解决方案有两种。一种是主动给锁续期也就是上面说过的看门狗机制把这件繁琐的事交给 Redisson 去处理。另一种是从业务层面保证即使并发穿透也不会产生严重后果也就是幂等控制数据库加唯一索引、状态机校验、操作日志去重等。我的观点是分布式锁不能作为数据一致性的唯一保险它应该是第一道防线下游的幂等才是最后兜底。这样即使锁真的穿透了也不会产生资损级别的故障。6.2 主从切换导致的锁丢失少见的但要心里有数另一个隐蔽的坑是 Redis 主从切换场景。客户端 A 向主节点写入锁但主节点还没同步到从节点主节点宕机了从节点升为主节点此时从节点上没有 A 的锁记录客户端 B 就能轻松加锁成功A 和 B 同时持有锁。这在极少数情况下会发生但对数据一致性要求极高的系统是有致命影响的。解决这个问题的思路是 RedLock 算法或者使用强一致性的存储方案替代 Redis。但从成本收益角度来看大多数业务场景不会出现“主节点刚收到锁写入请求、立即宕机、从节点完成选举”这种极端时间窗口。大多数团队的做法是接受这个极低概率风险同时用数据库唯一约束在底层兜底。我自己也是这种思路不会因为这个小概率风险就把系统复杂度拉高一个档次。6.3 时间被回拨或跳跃Redis锁也会跟着犯迷糊还有一种隐藏场景是 Redis 服务器所在机器的系统时间被人为调整或 NTP 时间同步发生跳跃。Redis 的过期键依赖系统时间如果系统时间突然向前跳跃几十秒可能导致原本还有 20 秒生命的锁瞬间过期引发并发穿透。虽然这种情况不常见但一旦发生排查起来极其头疼因为代码看起来没有任何问题Redis 也显示正常只有深挖系统时间才能发现端倪。遇到这类问题我的排查思路是先看 Redis 的INFO命令里的uptime_in_seconds和日志有没有异常重启痕迹再检查服务器的时间同步配置。如果是云服务器一般不会有手动调时间的行为如果是自建机房、离线部署的环境就要格外小心。预防手段是让运维尽量使用标准 NTP 服务并监控时间偏移量。6.4 锁滥用导致的性能雪崩锁不是万能的“并发神器”最后说说我观察到的一种要不得的倾向很多人学会 Redis 锁以后遇到并发问题就加锁不管场景合不合适。结果是分布式锁招来了性能问题和各种复杂故障。判断一个场景该不该用分布式锁我有个简单原则先看并发冲突的概率有多大再看冲突造成的代价有多高。如果一个操作本身发生并发冲突的概率极低或者冲突最多只是浪费一点计算资源那不如用乐观锁或者直接放行靠幂等去重就够了只有在冲突会造成严重错误、且概率不可忽略时才值得引入分布式锁。对 Redis 锁的正确态度是它是工具箱里的一把好扳手但你不可能每个问题都拿扳手解决。我早期在一个库存同步场景里因为觉得 Redis 锁简单就把所有库存操作都加了锁结果明明可以靠DECR原子命令解决的场景白白损失了一半以上的吞吐量最后是优化掉锁才把性能拉回来的。这个教训让我很长一段时间里看到代码里出现锁第一反应都是“这里真的需要锁吗”。7. 关于Redis锁我给新手的几条实战箴言如果你刚接触分布式锁不久我建议你按这个顺序学习先手写一个基于SET NX EX和 Lua 脚本的简易锁把原理吃透再用 Redisson 的 RLock 替换掉手写版本体验看门狗和可重入的便利最后去了解 RedLock 的动机和争议理解分布式锁在极端场景下的理论边界。日常编码中请牢牢记住锁一定要设置过期时间锁的 value 必须唯一锁的释放必须安全只能释放自己的锁锁的粒度尽量精细化锁内不放大耗时操作。这五条朗朗上口是我带团队时反复强调的规范也是无数次线上故障换来的经验。我个人的体会是分布式锁是那种“看起来简单、用起来处处是坑”的技术。你在网上看到的标准答案只是冰山一角真正的功力和敬畏心全在那水面之下的边界案例里。每个场景的锁方案背后都是对业务一致性、系统可用性和运维复杂度的综合权衡。希望这篇文章能帮你把这些权衡想清楚少走几个我走过的弯路。