Redis 分布式锁:加锁、续期与安全释放

进程内的 synchronized 只能协调同一个 JVM 中的线程。服务部署成多个实例后,实例 A 和实例 B 各自持有一把本地锁,彼此看不见。Redis 分布式锁可以让多个进程围绕同一个 Redis 键协调,但它有清晰的能力边界。

1. 最基本的加锁条件

客户端尝试以原子方式创建一个不存在的键,并同时设置过期时间。概念上相当于:

1
SET lock:order:123 <unique-token> NX PX 10000

NX 表示仅当键不存在时设置,PX 设置毫秒级租约。值要使用每次加锁唯一的 Token,用来识别锁的所有者。若命令成功,当前调用者获得锁;若键已存在,则本次未获得锁。

过期时间能避免进程崩溃后永久占锁,但它不是正确性的全部。业务执行超过租约后,锁可能过期并被其他客户端获得,原持有者仍可能继续执行。

2. 为什么不能直接 DEL

假设客户端 A 的锁过期,客户端 B 随后拿到同一个锁键。若 A 恢复后直接执行 DEL lock:order:123,它会删掉 B 的锁。释放锁必须同时检查 Token 是否仍属于自己,并且比较与删除应作为一个原子操作。

Redis Lua 脚本的思路如下:

1
2
3
4
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0

调用方将锁键和自己的随机 Token 传入脚本。不要拆成“先 GET,再 DEL”两条独立命令,否则两步之间仍有竞态窗口。

3. 锁租约和长任务

租约应结合任务时长、故障恢复和最大并发设计。固定租约过短可能使任务尚未结束锁就失效;过长则延缓故障后的恢复。Redisson 等客户端可以提供续期机制,但续期线程、进程暂停、网络分区和 Redis 故障仍需要纳入设计与监控。

4. 需要更强保护时使用 fencing token

当旧持有者可能在锁过期后继续写入共享资源时,仅检查锁状态不一定能阻止旧操作。Fencing token 是单调递增的令牌,资源端拒绝小于已接受令牌的写入。它需要资源本身支持校验;单靠普通 Redis 锁键并不能自动提供这一保护。

5. Redis 锁不等于业务唯一性

重要业务通常还应由数据库约束、条件更新或幂等键兜底。例如“一人一单”可用唯一索引防止重复记录,库存可用带条件的原子更新避免减成负数。锁用于减少竞争或串行化某段逻辑,数据约束才是持久化层的最终防线之一。

6. 何时不必自己造锁

如果应用已有成熟客户端,应先了解其可重入、等待、续期、取消和解锁语义,不要照着几条命令拼出一个看似完整的实现。对于需要严格一致性的关键流程,应评估数据库锁、队列单线程分区或其他协调服务。

参考:Redis 分布式锁模式。