Redis 分布式锁的正确实现:从 SETNX 到 Redisson 看门狗

一、错误示范:先 setnx 再 expire

Java
// ❌ 非原子:setnx 成功但 expire 前宕机 → 死锁
if (redis.setnx("lock", "1") == 1) {
    redis.expire("lock", 30);
    // 业务
    redis.del("lock");
}

二、正确姿势:原子命令

Bash
SET lock_key unique_value NX PX 30000
Java
String result = jedis.set(key, requestId, "NX", "PX", 30000);
if ("OK".equals(result)) {
    try {
        // 业务逻辑
    } finally {
        // Lua 保证原子删除
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, Collections.singletonList(key),
                   Collections.singletonList(requestId));
    }
}

为什么必须校验 value:防止 A 线程超时后,B 获得锁,A 恢复后误删 B 的锁。

三、锁续期(Watch Dog)

业务执行时间超过锁超时时间的解决方案:
Java
private void scheduleExpirationRenewal(String key) {
    Thread renewal = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Thread.sleep(10000);  // 每 10s 续一次
                jedis.pexpire(key, 30000);
            } catch (InterruptedException e) {
                break;
            }
        }
    });
    renewal.setDaemon(true);
    renewal.start();
}

四、直接使用 Redisson

Java
@Resource
private RedissonClient redisson;

public void seckill(Long productId) {
    RLock lock = redisson.getLock("seckill:" + productId);
    try {
        // 默认 30s,看门狗每 10s 自动续期
        boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
        if (!locked) {
            throw new BusinessException("系统繁忙,请重试");
        }
        // 扣库存
    } finally {
        lock.unlock();
    }
}

Redisson 帮我们做了:

  1. 原子加锁(Lua 脚本)
  2. 哈希结构支持可重入
  3. Watch Dog 自动续期
  4. 订阅解锁消息,减少无效轮询

五、Redlock 之争

Redis 作者提出的 Redlock(多数派加锁)被 Martin Kleppmann 撰文质疑:
  • 时钟跳跃可能导致锁失效
  • GC Stop-The-World 可能让客户端"以为"还持有锁
结论:
  • 追求性能与容错:单实例 + 主从复制足够(接受极小概率丢失)
  • 追求强一致:使用 Zookeeper / etcd 或数据库乐观锁

六、选型建议

| 场景 | 方案 | | --- | --- | | 防止重复提交 | Redis SET NX | | 秒杀扣库存 | Redis 锁 + Lua 原子扣减 | | 定时任务防重 | 数据库唯一索引 | | 金融级强一致 | Zookeeper / 数据库锁 |
打赏作者 已有 0 人打赏,共 ¥0.00
我的打赏
DBA老赵
DBA老赵
Lv7 学习会员 原创 2 粉丝 603

MySQL / Redis 调优,数据库就是我的战场

  • 2文章
  • 4.9万总阅读
  • 1716获赞
  • 603粉丝

评论(2)

DBA老赵・澳大利亚
对,这是个常见错误:unlock 时线程已经不持有锁会抛 IllegalMonitorStateException。
24天前 👍 42 回复 举报
Go语言爱好者・澳大利亚
Redisson 的看门狗机制确实省心,不过要注意别在 finally 里忘了判 isHeldByCurrentThread。
25天前 👍 38 回复 举报