在分布式微服务架构中,多个服务实例同时操作共享资源(例如超卖扣减库存、秒杀防重复下单、账单批量对账)时,单机的互斥锁(如 Go 的 sync.Mutex 或 Java 的 ReentrantLock)便无法跨机器生效,必须引入分布式互斥锁。
1. 初代错误姿势:简单 SETNX + EXPIRE
早期最常见的反模式代码如下:
# 错误示例:非原子操作
if redis.setnx("order_lock", "1"):
redis.expire("order_lock", 10) # 若执行完 setnx 机器挂掉,造成永久死锁!
do_business()
redis.delete("order_lock")
从 Redis 2.6.12 起,官方支持了扩展的单条原子 SET 命令:
# 原子加锁:PX 毫秒超时,NX 键不存在才执行,value 必须为客户端唯一生成的 UUID
SET resource_name my_unique_token NX PX 30000
2. 释放锁的致命陷阱:误删他人锁
假设线程 A 业务执行超过 30 秒,锁自动过期被释放;此时线程 B 成功获取了锁;随后线程 A 业务执行完毕直接调用 DEL resource_name,此时被删除的其实是线程 B 正在持有的锁!
必须校验 Value 是否等于自己生成持有的 unique_token,并在 Lua 脚本中保证比较和删除的原子性:
-- 安全释放锁的 Lua 脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
3. 自动续期:看门狗(Watchdog)机制
如果业务耗时存在不确定性,固定超时时间总有越界风险。像 Java 的 Redisson 框架通过后台定时器(Watchdog),在锁有效期剩余 1/3 时异步发起 pexpire 续约。直到业务线程主动解锁时,取消守护定时器。
4. 主从架构丢失与 Redlock 争议
由于 Redis 的主从复制是异步的:如果在主节点写入锁成功后、尚未同步到从节点前,主节点突然宕机,哨兵/集群触发故障转移选举从节点为新主,此时另一个客户端就可以在新主上再次获取同一个锁,导致互斥性失效。
为此 Salvatore 设计了跨 5 个独立无关联节点的 Redlock 算法。分布式专家 Martin Kleppmann 曾指出在网络分区、GC 暂停、时钟跳跃下 Redlock 仍有潜在安全漏洞。生产环境总结:如果要求 100% 绝对一致性(如金融出金),应优先使用 ZooKeeper/etcd 或数据库强事务;而对于绝大多数互联网高并发业务,带 Lua 脚本与唯一 Token 的单主 Redis 已经完全足够。