做后端这几年,Redis 分布式锁是绕不开的一道坎。尤其面试的时候,十次有八次会被问到“Redis 的分布式锁有哪几种实现方案,具体什么场景该用哪种”。这题看着简单,但真正能答得有层次、能落到工程实践上的人不多。今天不搞花架子,直接用 4 种实现方案把这块补齐:从最原始的 SETNX 加锁,到 Lua 脚本保证原子释放,再到 Redisson 的看门狗续期,最后聊一下争议很大的 RedLock 红锁。每种方案把原理、代码、适用场景和坑都讲透,适合正在准备分布式锁面试题的开发者,也适合项目里想上分布式锁但还没想清楚技术选型的人。
1. 分布式锁到底在解决什么问题,为什么偏偏是 Redis
1.1 一个并发扣减场景引发的思考
先说一个最典型的场景:库存扣减。你在订单服务里写了“查询库存、判断是否大于 0、扣减库存”这三步逻辑,单机部署的时候,用 synchronized 或者 ReentrantLock 就能锁住,因为 JVM 里只有一个进程,线程锁天然互斥。
但一旦服务做了多副本部署,问题立刻变了。订单服务三个节点,同时来了两个请求,两个节点的线程可以同时读到库存还有 1 件,然后各自在自己的 JVM 里通过锁检查,结果都认为可以扣,最终库存变成 -1。这就是典型的“并发在不同进程里”的场景,JVM 锁管不住别的进程。
分布式锁要解决的,就是让多个进程、多台机器在访问同一个共享资源时,只允许其中一个线程真正操作,其他线程要么等待、要么快速失败。更严格地说,分布式锁需要满足几个硬性条件:第一是互斥,任何时刻只能有一个客户端持有锁;第二是防死锁,持有锁的客户端宕机或者超时了,锁必须能自动释放,不能让其他客户端永远等下去;第三是防误删,客户端释放锁时只能释放自己持有过的锁,不能把别人的锁删掉;第四是“可重入”作为可选能力,同一线程可以重复获取同一把锁而不产生死锁。
这四条里,最容易踩坑的是第二条和第三条。很多文章只讲“用 SETNX 加锁”,但真正落到生产环境,你会发现只加锁不解释释放细节,事故一个接一个。
1.2 为什么选 Redis 而不是数据库或 ZooKeeper
说到分布式锁,绕不开一个选型问题:为什么大家默认用 Redis,而不是用数据库自带的行锁、悲观锁,或者 ZooKeeper 的临时顺序节点?
数据库方案最朴素,直接在业务表上 SELECT ... FOR UPDATE,或者搞一张独立的锁表。听起来简单,但性能和连接资源是硬伤。一次锁操作占用一个数据库连接,高并发下连接池先被打满;而且数据库锁的超时处理、死锁检测都比较笨重,在秒杀这种量级下基本不敢用。
ZooKeeper 方案在强一致上确实比 Redis 强,它通过 ZAB 协议保证写操作被多数节点确认,临时顺序节点可以天然做到“客户端断开后锁自动消失”。但代价也很明显:需要额外维护一套 ZK 集群,客户端 SDK 更重,一次分布式锁操作要经历多次网络和磁盘同步,延迟明显高于 Redis。
Redis 之所以成为默认选项,核心原因是它把性能和控制力平衡得非常好。Redis 是单线程模型处理命令,命令执行天然串行原子,SETNX 这种“不存在才写入”的原语完美契合加锁需求;单次操作的延迟在微秒级,配合主从、哨兵、集群可以搭建高可用架构。更重要的是,很多团队本来就用 Redis 做缓存和中间件,顺带承担分布式锁职责,运维成本几乎为零。
Redis 的数据类型里,分布式锁最常用的是 String 和 Hash:String 用于简单的 Key-Value 加锁,Hash 被 Redisson 用来实现可重入计数。理解这一点,后续看 Redisson 源码会轻松很多。
1.3 和分布式锁强相关的三个 Redis 基础原语
在展开 4 种方案之前,先把三个基础原语讲明白,后面所有代码和解释都依赖它们。
第一是 SETNX,全称是 SET if Not eXists。SETNX key value 只有在 key 不存在时才会写入,如果 key 已经存在,则直接返回失败。这个“不存在才写入”的语义,天然就是“互斥”的数学建模。
第二是 SET key value NX EX ttl。这条命令是把加锁和设置过期时间合并成一个原子操作。为什么要合并?因为老的写法是先 SETNX,再单独执行 EXPIRE 设置过期时间,这两条命令之间如果进程崩溃,锁就会变成永久锁,其他线程再也拿不到。Redis 2.6.12 之后,SET 命令扩展了 NX、EX、PX 这些参数,就是为了解决这个原子性问题。
第三是 Lua 脚本。Redis 的 Lua 脚本在执行期间不会被其他命令打断,脚本内的多条 Redis 命令作为一个整体原子执行。分布式锁的“安全释放”必须依赖 Lua:先检查 value 是否是自己写的,是才删除,这两步如果不原子,就会在“检查通过”和“执行删除”之间插入别人的操作,造成误删。
这三个原语理解透了,4 种实现方案其实就是把它们按不同复杂度组合起来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 4 种实现方案逐个说透
2.1 方案一:最原始的 SET NX EX 原子加锁
第一种方案是最容易理解的入门款,也是你去看很多老教程经常见到的写法。加锁就一条命令:
bash复制SET order:pay:123 uuid-xxx NX EX 30
这条命令的含义是:如果 order:pay:123 这个 key 不存在,就写入 uuid-xxx,同时设置 30 秒过期时间;如果 key 已经存在,直接返回 nil,表示加锁失败。整个过程原子执行,不存在先加锁、后设置过期时间的中间状态。
用 Java 的 Spring Data Redis 写出来大概是这样:
java复制String lockKey = "order:pay:123";
