做后端这几年,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";
String clientId = UUID.randomUUID().toString();
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, Duration.ofSeconds(30));
if (Boolean.TRUE.equals(locked)) {
try {
// 业务逻辑
} finally {
stringRedisTemplate.delete(lockKey);
}
}
这段代码看起来没问题,但你注意看 finally 里的 delete,这里藏着一个经典事故。如果线程 A 持有锁之后业务执行超过了 30 秒,锁自动过期了,线程 B 成功加锁开始执行业务;等到线程 A 终于执行完,调 delete 直接把 B 的锁删掉了。再然后线程 C 又成功加锁,三个线程同时在跑同一个临界区。
正确的做法是,value 必须使用一个客户端唯一标识(比如 UUID + 线程 ID),删除锁之前先确认 value 还是自己的,确认完整判断后删掉。但“先 GET 判断、再 DELETE”这两步不是原子操作,需要在 Lua 脚本里完成。所以方案一在实际项目中很难单独落地,它更适合用来理解分布式锁的基本原理,理解“互斥 + 过期时间”这两个核心概念。
2.2 方案二:SETNX + Lua 脚本实现安全释放
方案二是在方案一的基础上,把释放锁的操作重构成 Lua 脚本。加锁逻辑不变,释放逻辑变成这样:
lua复制-- lock_key: 锁的 key
-- client_id: 当前线程持有的唯一标识
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
执行这条 Lua 脚本,Redis 会保证“GET 判断”和“DEL 删除”中间不会插入其他客户端命令,从根上解决了误删问题。配合 Java 侧的逻辑:
java复制String lockKey = "order:pay:123";
String clientId = UUID.randomUUID().toString();
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, Duration.ofSeconds(30));
if (Boolean.TRUE.equals(locked)) {
try {
// 业务逻辑
} finally {
// 执行上面 Lua 脚本释放锁
stringRedisTemplate.execute(
releaseScript,
Collections.singletonList(lockKey),
clientId
);
}
}
到这里,一个“互斥 + 防误删 + 防死锁”的简单分布式锁就成型了。唯一没解决的是“业务执行时间超过锁过期时间”的问题。这事的本质是:过期时间完全靠人工预估,但业务耗时有波动,预估多少都可能不准。
一个常见补充做法是“自动续期”:当锁的过期时间过了一半还没释放,就用一个后台线程去执行 PEXPIRE lockKey newTtl,把过期时间续下去,直到业务结束主动删锁。这个“定时续期”的思想,其实就是后面 Redisson 看门狗的雏形。自己实现的话,要注意续期线程必须随主业务线程的结束而停止,否则业务跑完了续期线程还在跑,锁永远不释放。我在早期项目里就踩过这个坑,续期线程没做原子控制,结果业务结束后锁还多活了十几秒,下游服务感觉很奇怪。
方案二适合那些不想引入额外依赖、业务相对简单、团队能接受自己维护续期逻辑的项目。它是很多框架底层自己实现的分布式锁模板,也是面试里如果让你“手写一个分布式锁”时最稳妥的答案。
2.3 方案三:Redisson 可重入锁 + 看门狗自动续期
方案二已经能覆盖大部分场景,但它有两个痛点:一是不可重入,同一线程在锁内再次获取同一把锁会失败;二是续期逻辑要自己写,写不好就出事故。Redisson 就是为了解决这些工程化问题而生的。
Redisson 的分布式锁底层没有用简单 String,而是用了 Hash 结构。key 仍然是锁名,field 是客户端唯一标识(UUID + 线程 ID),value 是重入计数。加锁时对 Hash 执行 HINCRBY,释放时逐步减一,减到 0 才删除整个 key。这样就实现了可重入:同一个线程锁内再锁,只是变量加一,不会触发互斥。
看门狗机制是 Redisson 的另一大核心。默认情况下,如果你调用 lock() 不传过期时间,Redisson 会给锁设定 30 秒的 leaseTime,然后启动一个后台定时任务,每 10 秒检查一次,只要锁还被当前线程持有,就自动把过期时间重新设置为 30 秒。这个续期动作一直持续到锁被显式释放,或者持有锁的线程 JVM 崩溃。也就是说,你不需要关心“业务跑 20 秒还是跑 2 分钟”,锁都不会因为没到期的业务而提前消失。
代码大致是这样:
java复制RLock lock = redissonClient.getLock("order:pay:123");
try {
// 等待 5 秒拿锁,拿不到返回 false
if (lock.tryLock(5, TimeUnit.SECONDS)) {
// 业务逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
实际使用时有一个很容易忽略的细节:如果你在 tryLock 里显式传了 leaseTime,看门狗就会失效,因为 Redisson 认为你已经明确指定了锁的存活时间,它不再需要帮你续期。很多人调试时发现“我传了 10 秒,为什么锁 10 秒就没了”,原因就在这里。
Redisson 是生产环境最推荐的方案,它把可重入、自动续期、公平锁、异步加锁都封装好了,你只需要关心业务逻辑。但也要注意:看门狗并不是万能保险。如果持有锁的线程发生了长时间 FullGC 或者网络阻塞,线程本身还活着,看门狗就会一直续期,外部流量只能在锁外面等待。所以实际项目里还是要给业务设置合理的执行超时,别让一个任务无限跑下去。
2.4 方案四:RedLock 多节点红锁,理论上的强一致方案
到了方案四,讨论已经从“怎么实现”变成了“怎么让锁更可靠”。前面三种方案都默认操作的是单个 Redis 节点。问题在于,主从架构下 Redis 主节点刚写入锁,还没来得及异步复制给从节点就宕机了,从节点被提升为主节点后,它内存里没有这把锁。这个窗口内,其他客户端就能成功加上同一把锁,互斥被打破。
RedLock 的思路是:不再依赖单个 Redis 节点,而是向 N 个相互独立的 Redis 节点(通常是 5 个)同时申请锁。只有满足两个条件时才认为锁被成功获取:第一,成功加锁的节点数量大于 N/2;第二,整个加锁过程的耗时小于锁的过期时间。
流程拆开是这样:
- 获取当前系统时间,记为 startTime。
- 依次对 5 个独立的 Redis 节点执行
SET lockKey uniqueId NX PX expireTime,每个节点可能成功也可能失败,超时的节点直接跳过。 - 统计成功加锁的节点数。如果大于等于 3,并且“当前时间 - startTime”小于锁的过期时间,那么判定加锁成功。
- 加锁成功后,真正的锁有效剩余时间等于“设置的过期时间 - 加锁总耗时”,业务必须在剩余时间内完成。
- 释放锁时,对 5 个节点统一执行 Lua 脚本释放,不能只释放成功的节点。
用伪代码表示大概是:
java复制int nodes = 5;
int successCount = 0;
long totalCost = 0;
String value = UUID.randomUUID().toString();
long start = System.currentTimeMillis();
for (RedisClient node : nodesList) {
boolean ok = node.set(lockKey, value, "NX", "PX", expireTime);
if (ok) successCount++;
// 单个节点加锁超过阈值,直接认为失败,继续下一个
if (System.currentTimeMillis() - start > expireTime) break;
}
totalCost = System.currentTimeMillis() - start;
if (successCount >= 3 && totalCost < expireTime) {
// 持锁成功
} else {
// 持锁失败,对所有节点释放锁
for (RedisClient node : nodesList) {
node.eval(releaseScript, lockKey, value);
}
}
RedLock 在理论上解决了“单点 Redis 故障导致锁丢失”的问题,但它在业界引发的争议同样非常大。分布式系统领域有影响力的专家写过专门的文章批评 RedLock,核心观点包括:分布式锁并不能解决所有一致性问题;算法依赖系统时钟,如果某个节点时钟发生跳跃,锁的有效期计算就不可靠;网络分区时可能出现两个客户端同时认为自己是锁持有者;与其追求一个看似完美的多节点锁,不如把精力放在让后端资源操作具备幂等性和最终一致性上。当然,RedLock 作者也有自己的回应,双方争论的细节很长。
我的建议是:如果只是为了准备面试,RedLock 作为“知识面”讲给面试官听没有问题;如果要在生产环境落地,除非你们对一致性要求极高、有专门的分布式系统团队,并且能接受性能和运维复杂度,否则不要轻易用 RedLock。对绝大多数业务,方案三的 Redisson 锁,再叠加数据库层面的唯一约束或者乐观锁兜底,性价比要高得多。
3. 场景对比:项目里到底怎么选
3.1 五个关键维度的横向对比表
把 4 种方案放在一张表里对比,项目选型就一目了然了。
| 对比维度 | 方案一:SET NX EX | 方案二:SETNX + Lua | 方案三:Redisson 可重入锁 | 方案四:RedLock 红锁 |
|---|---|---|---|---|
| 依赖复杂度 | 低,Redis 即可 | 低,Redis + 少量脚本 | 中,需要引入 Redisson 依赖 | 高,需要多个独立 Redis 节点 |
| 可重入 | 不支持 | 不支持 | 支持 | 取决于实现 |
| 锁过期续期 | 无 | 需自己实现续期线程 | 看门狗自动续期 | 靠缩短有效时间,续期复杂 |
| 释放安全性 | 有误删风险 | Lua 保证安全释放 | Hash 结构安全释放 | 多节点释放,复杂度高 |
| 单点故障影响 | 有,主从切换可能丢锁 | 有,主从切换可能丢锁 | 有,主从切换可能丢锁 | 相对最低,但非绝对 |
| 推荐场景 | 原理学习、极短临界区 | 内部系统、简单业务 | 绝大多数生产系统首选 | 强一致要求且能接受成本 |
这个表格里最需要关注的是“单点故障影响”那一行。很多人以为用了 Redis 主从、哨兵、集群部署形态,锁的安全等级就上去了,其实不是。锁的安全性和部署形态是两回事,主从复制默认是异步的,主节点挂了丢数据,锁照样可能丢。后面第 4 章会单独展开。
3.2 场景一:秒杀、抢购这类短临界区任务
先看最典型的高并发场景:秒杀、抢购、限量优惠券发放。这类业务的特点是锁的持有时间非常短,可能只有几十毫秒到几百毫秒,但并发量极大,每秒可能来几万次请求。
这种情况下,锁的 TTL 不需要很长,5 到 10 秒绰绰有余,甚至 3 秒都够用,因为业务逻辑就一个 Redis 操作加一个内存操作。方案二完全能扛住:加锁用 SET NX EX,释放用 Lua,value 用 UUID,TTL 设 5 秒。我自己实际做过一次压测,在单节点 Redis、8 核机器上,方案二的加锁释放吞吐可以稳定跑在每秒十万级别,瓶颈反而在业务代码里。
这里要特别注意锁粒度。很多人一个项目只用一个全局锁 key,比如 lock:stock:global,所有商品共用一把锁,那并发再高都会被锁串行化,性能直接废掉。正确的设计是把锁 key 细粒度化,比如 lock:stock:sku_1001、lock:order:user_8888,每一件商品、每一个用户的行为只影响自己那部分竞争。
秒杀场景还有个常见的血泪教训:不要把秒杀的全部逻辑都包在锁里。锁的作用只是保护“扣库存”这个临界区,像校验用户是否秒杀过、生成订单号这种操作,完全可以在锁外完成。锁内的代码越短,锁的争抢时间越短,系统整体吞吐越高。
3.3 场景二:微服务接口防重、定时任务唯一执行
微服务环境里,相同逻辑的接口可能会被网关重试、消息重复投递触发多次,这时候分布式锁用来做防重,价值非常大。典型的例子是支付回调:渠道方可能因为网络抖动连续回调好几次,如果每次回调都原样处理,用户可能被重复扣款或者重复发券。
方案三是这类场景的首选。Redisson 的可重入锁天然支持同一线程内的嵌套调用,比如防重接口内部又调用了一个加了同一把锁的公共方法,如果没有可重入,直接自己把自己锁死了。把 waitTime 设置得短一点,比如 2 到 5 秒,拿不到锁就直接返回“处理中”,让上游走重试逻辑,远远好过让请求一直挂在那儿等锁。
定时任务的唯一执行是另一个经典应用。一个数据对账任务部署在三个节点上,如果三个节点同时跑,就会重复处理同一批数据。解决思路很简单:所有节点尝试获取同一个锁 key,比如 scheduler:reconcile,只有获取成功的节点真正执行业务逻辑,另外两个节点直接跳过。
java复制RLock lock = redissonClient.getLock("scheduler:reconcile");
if (lock.tryLock(0, 30, TimeUnit.SECONDS)) {
try {
// 对账逻辑
} finally {
lock.unlock();
}
}
tryLock(0, ...) 表示不等待,拿不到锁立刻返回 false,正好符合定时任务的语义。
3.4 场景三:库存、资金这类强一致场景
这类场景是对分布式锁考验最严苛的地方。库存扣减、账户余额变动、优惠券核销,一旦出现并发问题,直接是资损事故。很多人第一反应是上 RedLock,但我在 2.4 小节已经说过,RedLock 并不是银弹,它解决的是“Redis 节点故障导致锁丢失”的问题,并不能解决“业务线程卡顿、锁过期导致临界区并发执行”的问题。
真正稳的做法是三层防御。第一层,用分布式锁把高并发的互斥冲突挡在 Redis 层面,减少数据库压力;第二层,让数据库自己具备最终一致性保护,比如扣库存用 UPDATE t_stock SET stock = stock - 1 WHERE sku_id = ? AND stock > 0,这条 SQL 本身就保证了不能扣成负数,即使锁失效,数据库也能兜底;第三层,针对关键操作做幂等设计,比如用户支付回调带上一个唯一请求号,数据库表对请求号建唯一约束,重复处理直接报错或者忽略。
我在一个企业电商项目里就是这么落地的。Redis 锁用 Redisson,锁 30 秒,业务正常 1 秒完成;Redis 挂了,还有数据库乐观锁踩着刹车;数据库主键冲突、唯一约束碰撞,还有幂等表接住。三层下来,任何一个单点故障都不会造成资损。
所以,如果你在面试里被问到“强一致为什么要用 RedLock”,可以顺势讲出这套分层兜底的思路,比单纯背书 RedLock 算法要有说服力得多。
4. 落地中的常见坑与排查实录
4.1 锁过期时间怎么定?业务没跑完锁却先过期
这是分布式锁落地时最常踩的坑。你把锁的过期时间设成 10 秒,结果业务里调了一个第三方接口,运气不好 30 秒才返回,锁早就没了,另一个线程进来把数据改了,你这边浑然不觉,等业务代码执行完还在 finally 里自信地把锁删掉。
锁过期时间的设置没有一个固定公式,我的经验是分三步评估:第一步,统计业务接口的历史耗时分布,取 P99 甚至 P99.9,不是取平均值;第二步,把 P99 再翻一倍,作为初始 TTL;第三步,引入自动续期机制,要么用 Redisson 的看门狗,要么自己写续期线程,让锁的有效期动态适配业务耗时,而不是固定死一个值。
评估周期也很重要。线上数据是波动的,大促期间的业务耗时往往比平时涨好几倍,你以为设了 30 秒很保险,大促一来一个查询就要 40 秒。所以锁的 TTL 最好和监控系统联动,一旦发现锁等待超时比例升高,优先看是不是锁过期太快。
4.2 主从切换与锁丢失:持久化怎么取舍
在 2.4 小节提到过主从异步复制导致的锁丢失问题,这里展开讲清楚。默认部署是“一主一从”,所有写操作走主节点,从节点通过复制流异步同步数据。假设主节点刚成功写入锁 key,客户端还没拿着锁开始干活,主节点就宕机了,哨兵提升从节点为新主,但新主节点内存里根本没有这把锁。这时候另一个客户端来加锁,能成功,两个客户端同时认为自己是唯一持有者。
要缓解这个问题,有两个方向。一个方向是提高 Redis 的持久化保障,比如开启 appendonly yes 并且把 appendfsync 设置为 everysec,把数据丢失窗口压缩到一秒以内;更极端的是 always,每次写都刷盘,但性能损耗不可接受,一般不推荐。另一个方向是在主从复制层面做处理,比如使用 WAIT 命令等待至少一个从节点确认写入完成后再返回成功,但这会显著增加加锁耗时,而且如果从节点不可用,主节点写也会被阻塞。
Docker 部署 Redis 主从、K8s 里跑 Redis 集群,都只是把部署形态变复杂了,并不会把异步复制变成同步复制。面试中如果被问到“Redis 集群部署了是不是就安全了”,一定要把这个点答出来:锁的可靠性和你的数据持久化、复制机制强相关,不是部署形态决定的。
4.3 误删锁、可重入与性能退化
误删锁是出现频率最高的问题。代码库里经常能看到这种实现:
java复制if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
先 hasKey 判断,再 delete,这两步在并发环境下毫无意义,判断完到删除之间锁可能已经归属于别人了。正确做法永远是 Lua 脚本校验 value,不校验 value 的删锁操作等于没有防护。
可重入问题在嵌套调用的场景很容易忽略。一个订单处理方法加了锁,方法内部调用了一个加同一把锁的服务方法,如果你的锁实现不可重入,第二次加锁直接失败,如果你没有处理加锁失败的分支,程序会继续执行未受保护的逻辑,风险更大。用 Redisson 可以避免这个问题,但如果坚持自己实现,就要在加锁时记录线程标识和计数,释放时递减。
性能退化一般分两类。一类是锁粒度太大,比如一个全局锁保护所有订单,导致订单 A 的处理要等待订单 B 的锁释放;另一类是锁内逻辑太重,比如锁内执行慢 SQL、冷数据加载、远程调用。锁内逻辑一旦放大,Redis 的互斥能力会成为系统最大的瓶颈,解决办法是“能放锁外的都放锁外”,让锁内只留真正需要互斥的几行代码。
4.4 面试高频考点速查
把常见的分布式锁面试题整理成一张速查表,方便你临场调用:
| 高频问题 | 答题要点 |
|---|---|
| Redis 分布式锁的实现原理是什么? | 用 SET NX EX 保证原子加锁,用 Lua 脚本保证原子释放,value 用唯一标识防误删 |
| 为什么不能用 SETNX 加锁、EXPIRE 设过期时间? | 两条命令不原子,中间崩溃会导致锁永久存在 |
| 分布式锁怎么防止误删别人的锁? | 加锁 value 用 UUID,释放前用 Lua 判断 value 是否一致 |
| 锁过期了业务没执行完怎么办? | TTL 按耗时 P99 的倍数设置,配合看门狗或续期线程自动续期 |
| Redis 主从切换时锁会丢吗? | 会,异步复制导致主节点锁没同步给从节点;可考虑 RedLock 或持久化加固 |
| 如何实现分布式锁的可重入? | 用 Hash 结构存储线程标识和重入计数,Redisson 即是这么做的 |
| 为什么说 Redis 单线程模型适合做锁? | 单线程执行命令天然串行,不存在命令间交错问题 |
| RedLock 是否绝对可靠? | 有争议,依赖时钟和网络,生产环境建议叠加幂等兜底 |
另外还有一个容易忽略的实战问题:RedisTemplate 的序列化器不一致。锁 key 如果通过 StringRedisTemplate 写入,但读取时用 RedisTemplate<Object, Object> 且默认 JDK 序列化,key 实际存到 Redis 里的字节序列完全不同,GET 永远拿不到值,锁就完全失效。排查方法很简单:用 Redis 可视化工具看一眼 key 的前缀是正常的字符串还是带有 \xAC\xED 这类乱码头。我在一个项目里就是被这种问题坑了半天,最后才发现是序列化器配置不一致。
写在最后的一点个人习惯
最后分享一个我自己在项目里的固定做法。鉴权、防重、定时任务这类非资金场景,统一用 Redisson 可重入锁,TTL 不手动设置、交给看门狗,waitTime 根据接口耗时评估设成 2 到 5 秒;极简单的内部工具、一次性脚本,用方案二手写 SET NX EX + Lua,不引入多余依赖,但会明确注释“仅限无阻塞场景”;涉及库存、扣款等资金级操作,Redis 锁只当第一道闸,数据库乐观锁、唯一约束、幂等表三层兜底,锁挂了也不允许出资损。如果你刚开始接分布式锁,我建议先自己手写一遍方案二,再切到 Redisson,这样你既知道框架帮你解决了什么问题,也明白它没帮你解决什么问题。踩过几次锁过期、误删、主从切换的坑之后,你对这些设计取舍的理解会完全不一样。
