Redis 分布式锁如何实现?

#中间件#面试八股

Redis 分布式锁如何实现?

结论:最小可用实现只有两条命令——SET key value NX PX ttl 加锁,Lua 脚本"比对 value 再删除"解锁。工程上再补三件事:value 用唯一标识防止误删、TTL 与看门狗解决业务超时、锁粒度与等待策略解决争抢。Redisson 就是把这套细节封装好的产物,RedLock 则是在多主场景下对可用性的进一步加码。

一、四种实现路线

方案加锁命令优点硬伤
SETNX + EXPIRE两条命令简单非原子,中间崩溃会死锁
SET NX PX(推荐)一条命令原子加锁 + 过期需自己处理续期与误删
Lua 自写EVAL 封装可控、可扩展需要处理脚本缓存与集群 key 同槽
RedissonRLock.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 只解决多节点容错,且要接受时钟假设。
笔记加载中…