java 技术随笔

Redis 分布式锁实战:从 SETNX 的坑到 Redisson 看门狗与可重入锁

“用 Redis 做分布式锁,SETNX 就够了吧?”——这句话背后藏着无数线上事故。只 SETNX 不加过期时间会死锁、加过期时间又可能误删别人的锁、锁超时了业务还没跑完会并发穿透、主从切换还会丢锁。本文从最原始的 SETNX 讲起,一步步推导出正确的实现,最后落到 Redisson 的生产级用法。

一、为什么需要分布式锁

单机环境下可以用 synchronizedReentrantLock,因为它们锁的是同一个 JVM 内的对象。但应用做成多实例部署(集群)后,请求会落到不同进程,各自持有一把“本地锁”,互不感知,临界区就被并发进入。分布式锁要解决的就是跨进程、跨机器(甚至跨机房)的互斥。主流的实现载体有 Redis、ZooKeeper、etcd,本文聚焦使用最广的 Redis。

二、从零到一的四个进化版本

V1:只 SETNX(会死锁)

Boolean ok = redisTemplate.opsForValue().setIfAbsent("lock:order", "1");
if (Boolean.TRUE.equals(ok)) {
    try {
        doBusiness();
    } finally {
        redisTemplate.delete("lock:order"); // 崩溃后永远走不到这里
    }
}

问题:拿到锁的线程在 finally 之前崩溃/宕机,key 永远存在,其他线程永远等不到锁——死锁

V2:SETNX + 单独 EXPIRE(不是原子的)

redisTemplate.opsForValue().setIfAbsent("lock:order", "1");
redisTemplate.expire("lock:order", 30, TimeUnit.SECONDS); // 和上面不是一条原子命令!

问题:SETNX 成功但 expire 还没执行时进程崩溃,锁依然没有过期时间。必须用单条原子命令解决:

V3:SET key value NX EX(原子加锁)

// setIfAbsent(key, value, timeout, unit) 底层就是 SET key value NX EX seconds
Boolean ok = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);

问题依旧存在:业务执行超过 30 秒,锁自动过期被别的线程拿走,前一个线程结束时 delete 可能把别人刚抢到的锁删掉

V4:value 用唯一标识 + Lua 原子释放(正确版本)

String requestId = UUID.randomUUID().toString(); // 每个请求的唯一标识
Boolean ok = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(ok)) {
    try {
        doBusiness();
    } finally {
        releaseLock(lockKey, requestId);
    }
}

// 释放锁:比较 value 是“自己的”才删除,两步合一用 Lua 保证原子
private void releaseLock(String key, String requestId) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "return redis.call('del', KEYS[1]) else return 0 end";
    redisTemplate.execute(
        new DefaultRedisScript<Long>(script, Long.class),
        Collections.singletonList(key), requestId);
}

要点:加锁用一条原子命令并设过期时间;value 必须存唯一标识;释放时必须先校验是不是自己的锁再删,校验+删除必须用 Lua 原子完成。到这步已经可以应对绝大多数单节点 Redis 场景。

三、过期时间怎么定?看门狗续期

V4 还有一个隐患:业务耗时 > 过期时间(比如 30 秒)时锁就失效了。手动把过期时间设很大又会放大“持有者崩溃后锁长时间不释放”的风险。生产级做法是看门狗(Watch Dog)自动续期:持有锁期间,后台定时(如每 1/3 过期时间)把过期时间续回默认值,业务结束主动释放并取消续期。自己实现容易踩坑,直接用成熟框架。

// Redisson 分布式锁:默认 30 秒过期 + 看门狗自动续期(默认每 10 秒续 30 秒)
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
    // waitTime=3:拿不到锁最多等 3 秒;leaseTime=-1:启用看门狗续期
    if (lock.tryLock(3, TimeUnit.SECONDS)) {
        doBusiness();
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock(); // 释放并取消看门狗
    }
}

注意:tryLock(waitTime, TimeUnit) 不传 leaseTime 时才会启动看门狗;若手动传入 leaseTime,到期即释放、不会续期。看门狗能续期前提是持有锁的 JVM 还活着(每次续期都带着心跳),所以不会出现“进程死了锁还在”的情况。

四、可重入锁

同一线程内,方法 A 加锁后调用方法 B,B 也来抢同一把锁——如果是不可重入的实现,B 会把自己阻塞死。synchronized/ReentrantLock 天然可重入,而手写 SETNX 版本不支持。Redisson 的 RLock 基于 Redis 的 Hash 结构记录“持有线程 + 重入计数”,天然可重入:

// RLock 内部 key 类型是 hash:field=线程标识,value=重入次数
// 加锁:hincrby;释放:hincrby -1,减到 0 才 del key
redissonClient.getLock("lock:pay").lock(); // 同一线程可多次 lock/unlock 配对使用

五、Redlock 与“红锁争议”

Redis 主从架构下有一个经典漏洞:线程 A 在 master 上加锁成功,master 还没同步给 slave 就宕机,slave 晋升为 master,此时锁数据丢失,线程 B 又能加上同一把锁。Redis 作者提出的 Redlock 算法要求向 N 个(一般 5 个)独立 Redis 节点依次加锁,超过半数成功才算加锁成功——即使个别节点丢了锁,多数派仍可保证互斥。

但分布式领域大牛 Martin Kleppmann 曾公开质疑 Redlock:它仍依赖时钟、且在网络分区下有漏洞,建议用带“ fencing token”的方案(如 ZK 的自增序号 + 数据库版本校验)才能真正防并发。现实结论:

  • 对绝大多数业务(秒杀扣库存、防重复下单),单机 Redis + Redisson + 业务兜底(数据库唯一索引/乐观锁)已经足够
  • 锁本身就是“概率性正确”的兜底,真正的强一致要落到数据库约束上;
  • 对“锁丢了会死人”级别的场景(资金类强一致),优先考虑 ZooKeeper/etcd 或带版本号校验的机制。

六、方案选型速查

场景推荐方案
单节点 Redis、Java 单体集群Redisson RLock(看门狗 + 可重入)
不想引第三方库、逻辑极简手写 SETNX + Lua(V4 版本),过期时间给足并做监控
Redis 主从/哨兵、要求稍高Redisson + 数据库/状态机兜底;必要时 Redlock
强一致、资金敏感ZooKeeper / etcd 分布式锁
只在单个 JVM 内并发synchronized / ReentrantLock 即可,别引分布式锁

七、高频面试题速答

  • SETNX 怎么设置过期时间? 用 SET key value NX EX 单条原子命令,别 SETNX 后再 EXPIRE。
  • 为什么释放锁要用 Lua? “判断是自己的锁”和“删除”是两步操作,不用 Lua 则两步之间可能被打断,导致误删他人锁。
  • 锁过期了业务没跑完怎么办? 看门狗自动续期(Redisson),业务完成后主动释放。
  • 怎么实现可重入? Redis Hash + 线程标识 + 计数;直接使用 Redisson RLock。
  • 主从切换丢锁怎么办? Redlock 多数派方案;或结合业务状态机/数据库唯一约束做兜底。
  • 分布式锁能完全替代数据库幂等吗? 不能。锁是并发控制,幂等是结果约束,生产上两者常配合使用。

一句话总结:加锁要原子(NX EX)、解锁要对人(唯一 value + Lua)、持有要续期(看门狗)、强一致别依赖 Redis。把这条线串起来,Redis 分布式锁从原理到落地就都通了。

标签
Redis分布式锁Redisson高并发