Redis 分布式锁如何实现?
结论:最小可用实现只有两条命令——
SET key value NX PX ttl加锁,Lua 脚本"比对 value 再删除"解锁。工程上再补三件事:value 用唯一标识防止误删、TTL 与看门狗解决业务超时、锁粒度与等待策略解决争抢。Redisson 就是把这套细节封装好的产物,RedLock 则是在多主场景下对可用性的进一步加码。
一、四种实现路线
| 方案 | 加锁命令 | 优点 | 硬伤 |
|---|---|---|---|
| SETNX + EXPIRE | 两条命令 | 简单 | 非原子,中间崩溃会死锁 |
| SET NX PX(推荐) | 一条命令 | 原子加锁 + 过期 | 需自己处理续期与误删 |
| Lua 自写 | EVAL 封装 | 可控、可扩展 | 需要处理脚本缓存与集群 key 同槽 |
| Redisson | RLock.tryLock | 可重入、看门狗、公平锁 | 引入客户端依赖,需理解其行为 |
| RedLock | 多实例多数派 | 容忍单点故障 | 强依赖时钟,学术界仍有争议 |
二、正确姿势的演进
v1 SETNX lock 1 → 成功
EXPIRE lock 30 → 进程崩溃,锁永不过期
❌ 非原子
v2 SET lock <uuid> NX PX 30000
✅ 加锁原子;value 是本次请求的唯一标识
v3 解锁:先 GET 判断是不是自己的,再 DEL
❌ 判断与删除之间可能超时换主,仍会误删
v4 EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
✅ 比对与删除在同一脚本内原子完成
// 手写版:加锁 + 安全解锁
public class RedisLock {
private final StringRedisTemplate redis;
private final String token = UUID.randomUUID().toString();
public boolean lock(String key, Duration ttl) {
return Boolean.TRUE.equals(redis.opsForValue()
.setIfAbsent(key, token, ttl)); // SET key token NX PX ttl
}
public boolean unlock(String key) {
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ "return redis.call('del', KEYS[1]) else return 0 end";
Long r = redis.execute(new DefaultRedisScript<>(lua, Long.class),
Collections.singletonList(key), token);
return r != null && r == 1L;
}
}
三、Redisson 的封装与看门狗
RLock lock = redisson.getLock("lock:order:" + orderId);
boolean ok = lock.tryLock(3, TimeUnit.SECONDS); // 只等 3 秒
try {
if (!ok) { throw new BizException("操作太频繁"); }
// 业务逻辑,默认 leaseTime = -1 时启用看门狗,每 10s 续期到 30s
doBusiness();
} finally {
if (lock.isHeldByCurrentThread()) { lock.unlock(); }
}
看门狗时间线(默认 lockWatchdogTimeout = 30s,续期间隔 = 1/3 ≈ 10s)
t=0 加锁成功,TTL=30s,启动定时任务
t=10 续期到 30s
t=20 续期到 30s
...
t=40 业务结束,主动 unlock → 删除 key,取消定时任务
若进程被杀:定时任务消失 → t=30 锁自然过期,不会永久死锁
可重入的实现是 Hash:HSET lock <uuid:threadId> 重入次数,重入 +1、解锁 -1,减到 0 才删 key。
四、常见追问
- 问:为什么 value 必须是唯一值?答:防止 A 超时后 B 拿到锁,A 恢复执行 finally 时把 B 的锁删掉。比对 value 才能确认"锁还是我的"。
- 问:看门狗会不会无限续期导致死锁?答:只要进程存活,锁就一直被续;这正是设计意图——业务没结束不该放锁。进程崩溃后续期停止,锁在 TTL 内自然释放。
- 问:tryLock 的 leaseTime 传 -1 和不传有什么区别?答:传 -1 启用看门狗;显式传正数则不续期,到期自动释放,适合"明确知道最长耗时"的场景。
- 问:RedLock 值得用吗?答:它要求 N 个独立主节点中多数派加锁成功并考虑时钟漂移,能容忍单节点故障,但运维成本高;单实例或主从 + 业务幂等通常已够。若数据极度敏感,用 etcd/ZooKeeper 更稳。
- 问:主从切换会不会丢锁?答:会。主节点加锁成功后未同步就宕机,从节点升主后锁不存在,两个客户端可能同时持锁;解法见下一节的 fencing token。
- 问:锁的粒度怎么定?答:按"被保护资源"定,如订单 ID、库存 SKU;粒度太粗(全局锁)会退化成串行,粒度太细(字段级)则保护不住复合操作。
小结
- 加锁用
SET NX PX,解锁用 Lua 比对 value,两条铁律不能省。 - TTL 必须大于业务耗时,超过就用看门狗续期;进程崩溃时靠 TTL 兜底。
- Redisson 解决了重入、续期、等待与公平性;RedLock 只解决多节点容错,且要接受时钟假设。