【硬核实战】彻底搞懂 Redis 分布式锁:从 SETNX 到 Redisson 看门狗机制

摘要:在高并发场景下,如何防止商品超卖?分布式锁是必修课。本文将从最原始的 SETNX 讲起,剖析其缺陷,一步步带你深入 Redisson 框架的底层原理,特别是经典的“看门狗”机制。拒绝背八股文,实战代码带你起飞! 关键词:Redis, 分布式锁, Redisson, Java, 高并发

1. 为什么我们需要分布式锁?

想象一个经典的秒杀场景: 数据库里只有 1 个 库存,此时有 A、B 两个用户 同时点击购买。

如果代码只是简单的:

  1. 查询库存 (select * from stock where id=1)

  2. 判断库存 > 0

  3. 扣减库存 (update stock set num = num-1)

在单机环境下,我们可以用 Java 的 synchronizedReentrantLock 解决。但在微服务/分布式架构下,A 请求打到了服务器 1,B 请求打到了服务器 2,JVM 内部的锁就失效了。

这时候,我们需要一个所有服务都能访问到的第三方来充当“裁判”,这就是分布式锁

2. 为什么选择 Redis?

市面上常见的分布式锁方案有三种:

  1. 数据库唯一索引:性能差,容易死锁,不推荐。

  2. Zookeeper:可靠性高(CP模型),但性能不如 Redis,且实现复杂。

  3. Redis:性能极高(AP模型),实现简单,是目前互联网大厂的主流选择。

3. 从 SETNX 到 Redisson 的进化之路

3.1 原始阶段:SETNX

最简单的 Redis 锁利用了 SETNX (SET if Not eXists) 指令。

Bash

SET lock_key value NX PX 10000

致命缺陷

  1. 原子性问题:早期版本加锁和设置过期时间是两步操作,可能死锁。

  2. 锁误删:A 线程卡顿了,锁过期自动释放;B 线程拿到锁;A 恢复后删除了 B 的锁。

  3. 业务没跑完锁就过期了:这是最头疼的,如何给锁“续命”?

3.2 终极方案:Redisson

为了解决上述痛点,Java 社区诞生了 Redisson 框架。它不仅封装了复杂的 Lua 脚本保证原子性,还引入了自动续期的**看门狗(Watch Dog)**机制。

4. Redisson 实战代码演示

4.1 引入依赖

XML

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

4.2 业务代码实现(标准姿势)

这是面试和生产环境中的标准写法,请背诵!

Java

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class StockService {

    @Autowired
    private RedissonClient redissonClient;

    public void deductStock() {
        // 1. 获取锁对象 (定义锁的名称,粒度越细越好)
        String lockKey = "product_stock_lock_1001";
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 2. 尝试加锁
            // tryLock(等待时间, 自动过期时间, 单位)
            // 建议:不设置自动过期时间,激活看门狗机制
            boolean isLocked = lock.tryLock(3, TimeUnit.SECONDS);
            
            if (isLocked) {
                // 3. 执行业务逻辑
                System.out.println("获取锁成功,正在扣减库存...");
                Thread.sleep(5000); // 模拟业务耗时
            } else {
                System.out.println("获取锁失败,系统繁忙,请稍后再试");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 4. 释放锁 (关键:判断是否是当前线程持有的锁)
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
                System.out.println("锁已释放");
            }
        }
    }
}

5. 核心原理:什么是“看门狗” (Watch Dog)?

很多同学不理解,如果我的业务执行了 30 秒,但锁默认过期时间只有 30 秒,业务没跑完锁断了怎么办?

Redisson 的看门狗机制就是解决这个问题的:

  1. 当我们调用 lock.tryLock() 没有设置过期时间时,Redisson 默认会设置 30 秒过期(lockWatchdogTimeout)。

  2. Redisson 会启动一个后台定时任务(TimeTask),每隔 30 / 3 = 10 秒检查一次。

  3. 如果当前线程还持有锁,就自动把锁的过期时间重新设回 30 秒

  4. 这就像一只忠诚的看门狗,只要你还在跑业务,它就一直帮你“续命”。

流程图解 (Mermaid)

Code snippet

sequenceDiagram
    participant ThreadA as 线程A
    participant Redis as Redis服务端
    participant WatchDog as 看门狗(后台线程)

    ThreadA->>Redis: 申请加锁 (SETNX)
    Redis-->>ThreadA: 加锁成功 (默认30s过期)
    
    loop 每10秒检测
        WatchDog->>Redis: 线程A还在吗?
        Redis-->>WatchDog: 还在
        WatchDog->>Redis: 重置过期时间为30s (续命)
    end

    ThreadA->>Redis: 业务结束,释放锁 (DEL)
    Redis-->>ThreadA: 锁已释放
    WatchDog->>WatchDog: 停止续命任务

6. 避坑指南

  1. 不要在 tryLock 中设置 leaseTime:如果你显式设置了过期时间(如 lock.tryLock(10, 5, TimeUnit.SECONDS)),看门狗机制将失效!锁到期会强制释放。

  2. finally 块的重要性:无论业务代码是否抛出异常,必须在 finally 中释放锁,否则会导致死锁。

  3. 锁的粒度lockKey 最好带上商品 ID(如 product_101),不要直接锁整个 stock,否则并发性能会直线下降。

7. 总结

分布式锁是高并发系统的基石。虽然 Redis 官方还提出了红锁(RedLock)算法来解决主从切换丢锁的问题,但在实际绝大多数生产环境中,Redisson 的普通锁 + 看门狗机制 已经足够应对 99% 的场景。

Logo

更多推荐