Redis 分布式锁与 Redisson 实践:从 SET NX 到看门狗续期
本文面向企业级 Java 后端开发者,承接本系列前几篇接口幂等主题:幂等解决“同一请求重复提交只生效一次”,分布式锁解决“同一时刻只有一个线程进入临界区”。两者经常配合使用,但解决的问题并不相同。
一、为什么单机锁不够用
synchronized、ReentrantLock 只在同一个 JVM 进程内有效。多实例部署时,两个实例可能同时进入临界区。典型示例场景:
- 同一订单被两个实例同时处理,并发扣减库存;
- 定时任务在多个实例上同时触发,重复扫描同一批待处理订单。
这时需要一个所有实例都能访问的互斥点。Redis 是常用选择之一(数据库、ZooKeeper、etcd 等也可以做分布式协调,本文聚焦 Redis 方案)。
二、用 Redis 实现分布式锁的三个要点
1. 加锁必须原子完成。 用一条命令同时完成“不存在才写入”和“设置过期时间”:
SET lock:order:pay:12345 8f2c1a9e-7b4d-4c1a-9e2f-示例值 NX EX 30
分开执行 SETNX 再 EXPIRE 会在两条命令之间进程崩溃时,留下一把没有过期时间的锁。
2. 锁值必须唯一。 value 一般用 UUID(示例值)或“实例标识 + 线程标识”组合,解锁时用它校验“只能释放自己的锁”,避免误删别人持有的锁。
3. 解锁必须原子完成。 “先 GET 比较、再 DEL”不是原子的,中间可能被别的线程抢占。正确做法是在服务端用 Lua 脚本一次完成比较与删除:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
三、过期时间怎么定
锁的过期时间必须大于业务的最长执行时间,否则锁提前释放,并发线程会同时进入临界区;过长则持有锁的实例崩溃后,其他实例要等很久才能接手。
实际做法是按业务耗时分布取一个偏保守的值。示例:某下单接口临界区业务通常在 200 毫秒内完成,取 30 秒过期时间(示例值,请按自身业务的耗时分布评估,不要直接照搬)。更稳妥的做法是使用 Redisson 的看门狗自动续期,见下一节。
四、Redisson:开箱即用的 RLock
Redisson 的 RLock 是可重入锁:同一线程可以多次加锁,内部计数,全部 unlock() 后才真正释放。
// 配置示例(单机 Redis,地址与密码均为示例值)
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("your-password"); // 示例:请替换为真实密码,不要提交到代码库
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("order:pay:" + orderId); // 示例 key:按业务拼接主键
boolean acquired = false;
try {
// 示例值:最多等待 5 秒获取锁,租期 30 秒
acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!acquired) {
// 获取锁失败:直接返回“系统繁忙,请重试”,不要阻塞用户,也不要静默继续执行业务
return Result.busy();
}
// 临界区:查单、扣减、写库等
return process(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("获取分布式锁被中断", e);
} finally {
if (acquired) {
lock.unlock();
}
}
几个要点:
tryLock(waitTime, leaseTime, unit):waitTime是获取锁的最长等待时间,leaseTime是租期;两个时间都要按业务评估,示例中的 5 秒、30 秒均为示例值。- 未指定租期时看门狗生效:默认租期 30 秒,看门狗会定期续期(默认值以所用 Redisson 版本的官方文档为准,不同版本可能调整)。业务线程正常结束时释放锁;线程崩溃后续期停止,锁按租期自然过期,避免死锁。
unlock()必须放在finally中,并且只有加锁成功的线程才能unlock()。- 不要在业务代码里对同一 key 手动
DEL,会破坏“只能释放自己的锁”的校验。
五、四个常见坑
1. 锁 key 粒度过大。 用一把全局锁串行化所有订单,会把无关业务也排队。锁 key 必须带业务主键,例如 order:pay:{orderId},粒度最小化。
2. 锁没有过期时间。 SETNX 成功后进程崩溃、又无过期时间,锁永远不释放,只能人工处理。
3. 锁提前过期。 业务耗时超过租期又不用看门狗续期,两个线程会同时持有“同一把锁”。处理办法:缩短临界区、启用看门狗续期,或把长任务拆成多步。
4. 主从切换丢锁。 Redis 主从是异步复制,主节点宕机时尚未同步的锁可能丢失。对一致性要求极高的场景(示例:资金类核心操作),需要评估 RedLock 等多节点方案的时钟同步要求,或改用强一致的协调服务。RedLock 本身在业内也有争议,选型时要按业务容忍度评估,不要把它当作默认答案。
六、分布式锁与接口幂等是互补的
典型组合(示例流程):先用幂等 Token 拦截重复提交,再用分布式锁串行化同一订单的并发操作,业务完成后再落库幂等记录。锁防并发、幂等防重放,两层防护针对的是不同的问题。
七、落地检查清单
- 加锁与设置过期时间是否由一条原子命令完成;
- 解锁是否用 Lua 脚本校验锁值;
- 租期是否覆盖业务最长耗时,是否启用看门狗续期;
unlock()是否在finally中,且仅由加锁成功的线程执行;- 锁 key 是否包含业务主键、粒度是否最小;
- 锁获取失败的分支是否有明确返回,而不是静默继续执行业务。
小结
分布式锁的核心就三句话:加锁原子、解锁校验归属、租期覆盖业务耗时。Redisson 把这三件事做成了开箱即用的 RLock,但 key 粒度、租期取值、失败分支仍然要由业务方自己设计。本文示例代码基于 Spring Boot + Redisson 的常见用法整理,未引用任何实测性能数据;超时、租期等数值均为示例,请按自身业务的耗时分布与容灾要求评估后使用。
- 点赞
- 收藏
- 关注作者
评论(0)