0
0
0

Redisson 分布式锁:使用与实现原理

2026-10-26
2026-10-26
文章摘要
|

一、Redisson 简介

Redisson 是基于 Redis 的 Java 客户端,提供了分布式锁、分布式集合、信号量等功能。

相比手动使用 SET NX EX 实现分布式锁,Redisson 解决或改善了以下问题:

  1. 可重入:同一个线程可以重复获取同一把锁。

  2. 可重试:获取锁失败后,可以等待并重新尝试。

  3. 自动续期:通过 Watch Dog(看门狗)机制,避免正常业务执行时间超过锁的过期时间。

  4. 安全释放:只有持有锁的线程才能释放锁。

  5. 原子性:通过 Lua 脚本保证加锁、解锁等操作的原子性。

注意:Redisson 无法彻底解决 Redis 主从切换时可能发生的锁丢失问题。

二、引入 Redisson

Redisson 不属于 Java 8 自带的依赖,需要通过 Maven 引入。

1. Maven 依赖

以下使用兼容 Java 8 的 Redisson 3.x 示例版本:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.27.2</version>
</dependency>

2. 配置客户端

@Configuration
public class RedissonConfig {

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();

        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379")
                .setDatabase(0);

        // 看门狗默认超时时间为30秒
        config.setLockWatchdogTimeout(30000);

        return Redisson.create(config);
    }
}

如果项目已经使用 spring-boot-starter-data-redis,仍然可以引入 Redisson,但需要注意两者的客户端及连接资源是独立管理的。

三、Redisson 加锁与释放锁

1. 推荐使用 tryLock()

public void createOrder(Long userId) {

    RLock lock = redissonClient.getLock("lock:order:" + userId);

    boolean acquired = false;

    try {
        // 最多等待3秒获取锁
        acquired = lock.tryLock(3, TimeUnit.SECONDS);

        if (!acquired) {
            throw new RuntimeException("系统繁忙,请稍后重试");
        }

        // 执行业务逻辑
        System.out.println("创建订单成功");

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取锁被中断", e);

    } finally {
        if (acquired && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

说明:

  • getLock():获取锁对象,不会立即向 Redis 加锁。

  • tryLock():尝试获取锁。

  • unlock():释放当前线程持有的锁。

  • finally:确保业务结束后执行解锁逻辑。

推荐使用带等待时间的 tryLock(),避免获取锁失败时无限等待。

2. tryLock() 三种方法的区别

方法

获取锁失败后

过期时间

看门狗

tryLock()

立即返回 false

默认30秒

支持

tryLock(waitTime, unit)

最多等待指定时间

默认30秒

支持

tryLock(waitTime, leaseTime, unit)

最多等待指定时间

自定义

不支持

第一种:立即尝试获取锁

boolean success = lock.tryLock();

获取失败立即返回 false,不会等待锁释放。

第二种:等待指定时间获取锁

boolean success = lock.tryLock(3, TimeUnit.SECONDS);

最多等待3秒,获取成功后由看门狗自动续期。

第三种:等待指定时间,并设置锁的固定过期时间

boolean success = lock.tryLock(
    3,
    30,
    TimeUnit.SECONDS
);
  • waitTime = 3:最多等待3秒获取锁。

  • leaseTime = 30:获取成功后,锁的有效期为30秒。

指定 leaseTime 后,看门狗不会自动续期。即使业务没有执行完成,锁也可能在30秒后自动释放。

总结:

  • tryLock():不等待,获取不到直接返回。

  • tryLock(waitTime, unit):等待指定时间,支持自动续期。

  • tryLock(waitTime, leaseTime, unit):等待指定时间,锁按固定时间过期,不自动续期。

四、Redisson 的加锁流程

假设线程A和线程B同时申请同一把锁。

线程A申请锁
    │
    ▼
Redis判断锁是否存在
    │
    ├── 不存在 → 创建锁 → 获取成功
    │                         │
    │                         ▼
    │                     执行业务
    │                         │
    │                         ▼
    │                     释放锁并通知
    │
线程B申请锁
    │
    ▼
发现锁已被线程A持有
    │
    ▼
获取失败,订阅释放通知
    │
    ▼
等待通知或超时后重新尝试
    │
    ▼
重新竞争锁
    │
    ▼
获取成功后执行业务

具体流程:

  1. 线程A调用 getLock() 获取锁对象。

  2. 调用 lock() 或 tryLock(),Redisson 通过 Lua 脚本尝试加锁。

  3. Redis 中锁不存在,创建锁并设置 TTL,线程A获取成功。

  4. 线程B申请同一把锁,由于已被A持有,获取失败。

  5. 线程B通过 Redis Pub/Sub 订阅锁释放通知,并结合 TTL 进行等待和重试。

  6. 线程A完成业务,执行 Lua 脚本释放锁,并在完全释放后发布通知。

  7. 线程B重新竞争锁,成功后执行业务。

注意:Pub/Sub 只是通知等待线程重新尝试获取锁,并不是直接将锁交给某个线程。

五、Redisson 可重入锁的实现原理

Redisson 的可重入锁通过 Redis 的 Hash 数据结构实现,而不是普通的 String。

1. Redis 存储结构

Key:lock:order:1001

Type:Hash

Field:客户端唯一标识:线程ID
Value:重入次数

例如:

lock:order:1001
    │
    └── clientId:threadId → 1

其中:

  • Key:分布式锁的名称。

  • Field:区分不同客户端及线程。

  • Value:当前线程获取锁的次数。

2. 加锁判断逻辑

申请锁
  │
  ├── 锁不存在
  │     └── 创建Hash,计数器=1
  │
  ├── 锁已存在,且属于当前线程
  │     └── 计数器+1,可重入
  │
  └── 锁属于其他线程
        └── 获取失败,等待或重试

3. 可重入示例

RLock lock = redissonClient.getLock("myLock");

lock.lock(); // 第一次加锁,计数器=1
lock.lock(); // 第二次加锁,计数器=2

try {
    // 执行业务逻辑
} finally {
    lock.unlock(); // 计数器=1
    lock.unlock(); // 计数器=0,真正释放锁
}

可重入锁的核心原理:

同一个线程再次获取锁时,不会被自己阻塞,而是将 Hash 中的重入次数加1。

释放锁时,计数器减1。只有计数器归零,才会真正删除锁,并通知其他等待线程。

因此,加锁多少次,就需要解锁多少次。

六、Watch Dog 看门狗机制

1. 为什么需要看门狗?

假设:

  • 锁的 TTL 为30秒。

  • 业务执行需要60秒。

如果没有自动续期,锁在第30秒过期,其他线程就可能获取该锁,导致多个线程同时执行业务。

Redisson 通过 Watch Dog 自动续期来降低这种风险。

2. 工作原理

默认情况下,看门狗超时时间为30秒,通常约每10秒执行一次续期。

0秒:线程A获取锁,TTL=30秒
    │
10秒:看门狗续期,TTL重新设置为30秒
    │
20秒:看门狗续期,TTL重新设置为30秒
    │
30秒:看门狗续期,TTL重新设置为30秒
    │
40秒:看门狗续期,TTL重新设置为30秒
    │
50秒:看门狗续期,TTL重新设置为30秒
    │
60秒:业务执行完成,释放锁

工作流程:

  1. 线程成功获取锁,Redis 设置初始 TTL。

  2. Redisson 启动定时续期任务。

  3. 定时检查锁是否仍由对应线程持有。

  4. 如果仍持有,则通过 Lua 脚本更新 TTL。

  5. 业务完成后释放锁,并取消续期任务。

  6. 如果客户端宕机,无法继续续期,锁最终到期自动释放。

3. 哪些方法支持自动续期?

加锁方式

自动续期

lock.lock()

是

lock.tryLock()

是

lock.tryLock(3, TimeUnit.SECONDS)

是

lock.lock(30, TimeUnit.SECONDS)

否

lock.tryLock(3, 30, TimeUnit.SECONDS)

否

重点:只有不指定固定 leaseTime 的加锁方式,才会启用看门狗自动续期。

看门狗能够降低锁提前失效的风险,但不能完全防止长时间 GC 暂停、网络故障等情况导致的锁失效。

七、实际开发注意事项

  1. 合理设计锁粒度:例如 lock:order:用户ID,避免所有用户竞争同一把锁。

  2. 使用 finally 解锁:确保正常执行或发生异常时都能尝试释放锁。

  3. 使用 tryLock 控制等待时间:避免请求长期阻塞。

  4. 避免固定 TTL 过短:业务未执行完成时,锁可能提前过期。

  5. 注意主从一致性问题:Redis 主从异步复制可能导致故障切换后锁丢失。

  6. 重要业务增加数据库保护:库存扣减、余额更新等操作应结合条件更新、唯一约束、幂等控制等机制保障数据正确性。

另外,isHeldByCurrentThread() 只能检查当前时刻是否持有锁,不能保证检查后锁一定不会丢失。解锁过程仍需考虑异常处理。

八、面试总结

Redisson 是基于 Redis 实现的分布式锁工具,相比手动使用 SET NX EX,主要支持可重入、等待重试和自动续期等功能。

它通过 Redis Hash 存储锁信息,其中 Field 表示客户端和线程,Value 表示重入次数,并通过 Lua 脚本保证加锁和解锁的原子性。

当获取锁失败时,线程可以通过 Pub/Sub 机制等待释放通知,然后重新竞争锁,避免持续高频轮询。

Redisson 还提供 Watch Dog 看门狗机制,在没有指定固定锁过期时间时自动续期,减少业务尚未完成而锁提前过期的问题。

不过,Redisson 不能彻底解决 Redis 主从切换导致的锁丢失问题。对于一致性要求较高的业务,还需要结合数据库层面的保护措施。

核心记忆:Hash 实现可重入,Lua 保证原子性,Pub/Sub 实现等待通知,Watch Dog 实现自动续期。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论