🔒 别再裸写 setnx 了!Redisson 分布式锁实战:看门狗、红锁、联锁一网打尽

34 阅读4分钟

在分布式系统中,为了保证同一时间只有一个线程能执行某段代码(比如秒杀扣库存、定时任务不重复执行),我们必须使用分布式锁

很多同学在面试时能把 setnx 背得滚瓜烂熟,但一到生产环境,写出来的代码却漏洞百出:

  • ❌ 死锁:拿到锁后服务挂了,锁永远不释放。
  • ❌ 误删锁:A 线程执行慢,锁过期了;B 线程拿到锁;A 执行完把 B 的锁删了。
  • ❌ 原子性问题:判断锁和删除锁不是原子操作。

今天,我们不造轮子,直接上业界最成熟的方案 —— Redisson。它不仅解决了上述所有问题,还提供了**看门狗(Watch Dog)**机制,让你的锁“自动续期”。

🛠️ 为什么选择 Redisson?

相比于自己用 RedisTemplate 封装 setnx,Redisson 提供了像 JDK ReentrantLock 一样简单的 API,同时在底层帮你搞定了:

  1. 自动续期:业务没执行完,锁不会过期。
  2. 可重入:同一个线程可以多次加锁。
  3. 等待重试:获取锁失败时,自动等待并重试,而不是直接返回失败。

1. 🚀 快速上手:最简单的锁

引入依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.0</version>
</dependency>

代码实战:

@Autowired
private RedissonClient redisson;

public void killProduct(Long productId) {
    String lockKey = "lock:product:" + productId;
    RLock lock = redisson.getLock(lockKey);

    try {
        // 1. 加锁 (阻塞等待,默认过期时间 30s,开启看门狗)
        lock.lock();
        
        // 2. 执行业务逻辑 (模拟耗时 40s)
        // 因为有看门狗,锁会自动续期,不会因为 30s 到期而释放
        Thread.sleep(40000); 
        
        // 3. 扣减库存
        inventoryService.deduct(productId);
        
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 4. 释放锁 (一定要在 finally 中!)
        // 只能释放自己的锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

2. 🐶 核心原理:看门狗 (Watch Dog)

很多同学不理解“看门狗”是啥。简单说,就是Redisson 在后台起的一个定时任务

  • 默认行为:如果你调用 lock() 时没有指定过期时间,Redisson 会默认设置 30 秒过期。
  • 续期机制:看门狗每隔 30 / 3 = 10 秒就会检查一下:“哎,这线程还在跑吗?还在跑就给它把锁的时间重置回 30 秒”。
  • 作用:防止业务逻辑执行时间超长,导致锁意外过期,被其他线程抢占。

⚠️ 注意:如果你手动指定了过期时间 lock.lock(10, TimeUnit.SECONDS),看门狗机制会失效!锁到期会自动释放。

3. 🛡️ 进阶玩法:尝试锁 (TryLock)

在秒杀等高并发场景,我们不希望线程一直阻塞等待锁,而是希望“抢不到就立刻返回失败,或者等待一小会儿”。

public boolean tryKillProduct(Long productId) {
    RLock lock = redisson.getLock("lock:product:" + productId);
    
    try {
        // 尝试获取锁
        // waitTime: 最多等待 3 秒
        // leaseTime: 锁持有 10 秒后强制释放 (看门狗失效!)
        boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS);
        
        if (isLocked) {
            try {
                // 拿到锁了,处理业务
                inventoryService.deduct(productId);
                return true;
            } finally {
                lock.unlock();
            }
        } else {
            // 没拿到锁,快速失败
            return false;
        }
    } catch (InterruptedException e) {
        return false;
    }
}

4. 🔗 联锁 (MultiLock) & 红锁 (RedLock)

场景:你需要同时锁定 3 个资源(比如同时扣减 3 个商品的库存),要么全成功,要么全失败。

RLock lock1 = redisson.getLock("lock:product:1");
RLock lock2 = redisson.getLock("lock:product:2");
RLock lock3 = redisson.getLock("lock:product:3");

// 创建联锁
RLock multiLock = redisson.getMultiLock(lock1, lock2, lock3);

try {
    // 同时加锁,只要有一个失败,就全部失败
    multiLock.lock();
    // 业务逻辑...
} finally {
    multiLock.unlock();
}

注:RedLock (红锁) 是为了解决 Redis 集群主从切换导致锁丢失的问题,但在实际生产中,RedLock 性能损耗较大且争议较多,通常MultiLock 或 Zookeeper 锁 是更好的替代方案。

5. 读写锁 (ReadWriteLock)

场景:读多写少。比如商品详情,读的时候大家都能读,但写的时候(改价格)必须独占。

RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:detail");

// 读锁 (共享锁)
rwLock.readLock().lock();
// ... 读取数据 ...
rwLock.readLock().unlock();

// 写锁 (排他锁)
rwLock.writeLock().lock();
// ... 修改数据 ...
rwLock.writeLock().unlock();

💡 架构师建议

  1. 不要过度依赖分布式锁:锁是性能杀手。能用数据库乐观锁(CAS)、Lua 脚本解决的,尽量别用分布式锁。
  2. Key 的设计:锁的粒度越细越好。锁 lock:product:1001 比锁 lock:product 性能高一万倍。
  3. Finally 释放:这是铁律。无论业务是否异常,锁必须释放,否则就是线上事故。

掌握了 Redisson,你就拥有了驾驭分布式并发的“缰绳”。赶紧去优化你的秒杀接口吧!