Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格(In-Memory Data Grid),它提供了一系列的分布式数据结构和服务,其中分布式锁是其最核心、最强大的功能之一。
与我们手动使用 SETNX 等命令实现的简单分布式锁相比,Redisson 提供的锁机制更加健壮、易用,并且解决了许多分布式锁的经典难题。
1. 为什么需要分布式锁?
在单体应用中,我们可以使用 synchronized 或 ReentrantLock 来保证多线程对共享资源的互斥访问。但在分布式系统中,应用部署在多个服务器上,每个服务器都有自己的 JVM,本地锁无法跨进程、跨服务器生效。此时,就需要一个所有应用实例都能访问到的 "第三方" 来协调锁的获取与释放,这个 "第三方" 通常就是 Redis。
2. Redisson 锁的核心优势
Redisson 实现的分布式锁不仅仅是简单地在 Redis 中设置一个 key,它解决了手动实现锁时常见的几个痛点:
- 可重入性 (Reentrant):同一个线程可以多次获取同一把锁,而不会造成死锁。
- 自动续期 (Watch Dog):解决了 "锁过期但业务未执行完" 的问题。如果持有锁的线程在锁过期前还没执行完,Redisson 会自动为锁续期。
- 阻塞与非阻塞获取 :支持
lock()(阻塞等待) 和tryLock()(尝试获取,立即返回) 等多种获取锁的方式。 - 公平锁 (Fair Lock):可以保证多个线程按申请锁的顺序来获取锁,避免 "饥饿" 现象。
- 多种锁类型:除了可重入锁,还提供读写锁、信号量、闭锁等多种高级锁。
3. Redisson 锁的实现原理详解
我们以最常用的可重入锁 (RLock) 为例,深入其内部工作机制。
3.1 获取锁 (lock())
当一个线程尝试获取锁时,Redisson 内部会执行一段 Lua 脚本。使用 Lua 脚本的好处是可以保证操作的原子性。
核心逻辑:
- 尝试获取锁 :
- 它会使用 Redis 的
HSET命令,以锁的名称(例如"myLock")为 key,创建一个 Hash 数据结构。 - Hash 的 field 是当前线程的唯一标识(例如
threadId:uuid),value 是该线程获取锁的次数(重入次数)。 - 同时,它会使用
PEXPIRE命令为这个 key 设置一个过期时间(默认 30 秒),防止死锁。
- 它会使用 Redis 的
- 判断结果 :
- 如果
HSET命令返回 1 :表示这是第一次获取锁,并且设置成功。此时,线程成功获得锁,并启动一个 Watch Dog 定时任务。 - 如果
HSET命令返回 0 :表示锁已存在,并且当前线程就是持有锁的线程。此时,它会将该线程的重入次数加 1 (HINCRBY),并重置锁的过期时间。 - 如果锁已存在,但不是当前线程持有:表示锁被其他线程占用。此时,当前线程获取锁失败。它会订阅一个与该锁相关的 Redis Channel,然后阻塞等待,直到收到锁被释放的消息,再重新尝试获取锁。
- 如果
3.2 Watch Dog (看门狗) 机制
这是 Redisson 最精妙的设计之一,用于解决 "锁过期但业务未执行完" 的问题。
- 启动:当一个线程成功获取锁(非重入)时,会启动一个后台定时任务(即 Watch Dog)。
- 作用:这个任务会每隔一段时间(默认是锁过期时间的 1/3,即 10 秒)检查一下,如果当前线程仍然持有锁,就会自动将锁的过期时间重置为 30 秒。
- 停止 :当线程执行完业务逻辑,调用
unlock()方法释放锁时,这个 Watch Dog 定时任务会被取消。 - 效果:只要持有锁的线程没有宕机,并且业务还在正常执行,Watch Dog 就会一直为它 "续命",保证锁不会在业务完成前过期。
3.3 释放锁 (unlock())
当线程执行完业务逻辑后,会调用 unlock() 方法释放锁。
核心逻辑:
- 执行 Lua 脚本 :
- 脚本首先判断当前线程是否还是锁的持有者。
- 如果是,就将该线程的重入次数减 1 (
HINCRBY ... -1)。 - 减 1 后,如果重入次数大于 0,说明线程还在重入状态中,脚本会重置锁的过期时间并返回。
- 减 1 后,如果重入次数等于 0,说明线程完全释放了锁。脚本会执行
DEL命令删除锁的 key,并向与该锁相关的 Channel 发布一个消息,通知其他等待的线程锁已释放。
- 后续操作 :
- 当其他因获取锁失败而阻塞的线程收到这个消息后,会立即被唤醒,并再次尝试去获取锁。
- 成功释放锁后,当前线程的 Watch Dog 定时任务也会被停止。
4. 代码示例
使用 Redisson 锁非常简单,它的 API 设计得与 Java 原生的 ReentrantLock 非常相似。
java
运行
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonLockExample {
public static void main(String[] args) {
// 1. 创建 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 2. 获取锁实例
RLock lock = redisson.getLock("myDistributedLock");
try {
// 3. 获取锁
// lock.lock(); // 阻塞式获取,默认 30 秒过期
// 或者使用 tryLock,尝试获取锁,最多等待 100 秒,上锁后 30 秒自动解锁
boolean isLocked = lock.tryLock(100, 30, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
// 4. 执行临界区业务逻辑
System.out.println("线程 " + Thread.currentThread().getId() + " 成功获取锁,开始执行业务...");
Thread.sleep(5000); // 模拟业务耗时
System.out.println("线程 " + Thread.currentThread().getId() + " 业务执行完毕。");
} else {
System.out.println("线程 " + Thread.currentThread().getId() + " 获取锁失败。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 5. 确保锁被释放
// 注意:一定要在 finally 块中释放锁!
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("线程 " + Thread.currentThread().getId() + " 释放锁。");
}
}
// 关闭客户端
redisson.shutdown();
}
}
总结
Redisson 的分布式锁机制通过结合 Lua 脚本 (保证原子性)、Hash 数据结构 (实现可重入)、Watch Dog 定时任务 (自动续期)和 Redis 发布 / 订阅(高效通知)等多种技术,构建了一个功能强大、高可用且易于使用的分布式锁解决方案。它极大地简化了分布式系统中并发控制的复杂性,是 Java 开发者在构建分布式应用时的首选工具之一。
幽默记忆
记 Redisson 锁?别死啃概念了!咱们把它脑补成 "公司公共厕所抢位大战",保准你笑完就忘不了 ------
1. 先搞懂:为啥要搞 "分布式锁"?
以前公司就 1 个办公室(单体应用),厕所门后挂个 "有人" 牌(synchronized 锁),谁先拿到谁用,没毛病。现在公司扩成 10 层楼(分布式系统,多服务器),每层都有厕所,但大家要抢 "全公司唯一的 VIP 坑位"(共享资源,比如改库存)------ 总不能 1 楼的人锁了 1 楼厕所,2 楼的人还冲进去吧?这时候就得找个 "全公司都认的裁判"(Redis),而 Redisson 就是裁判手里的 "智能叫号机",专门管谁能进坑、能蹲多久。
2. Redisson 这 "叫号机" 牛在哪?(核心优势)
它可不是普通叫号机,是带 "人性化服务" 的:
- 可重入:自己人不用重新排队比如你进了坑,突然发现没带纸,开门去拿纸(同一线程再次请求锁)------ 裁判不会让你重新排,直接说 "知道是你,进去吧",还在小本本上记 "这人第 2 次进坑"(Hash 结构存线程标识 + 重入次数)。
- **自动续期:怕你蹲太久被 "赶"**裁判怕你蹲坑时突然接了个长电话(业务没执行完),特意派了个 "看门大爷"(Watch Dog)------ 每隔 10 秒(默认过期时间 30 秒的 1/3)就扒门缝问:"兄弟还在不?在的话给你续 30 秒坑位时间啊!" 只要你没走,坑位就一直是你的。
- 可堵可撤:不想等就走,想等就排想蹲坑时,你可以选 "死等"(lock ()):站在厕所门口等别人出来;也可以选 "问一句就走"(tryLock ()):问裁判 "现在有空吗?100 秒内没空我就走了",不耽误事。
- 公平锁:谁先来谁先上,不允许加塞要是公司有人总爱插队(非公平锁导致线程饥饿),裁判就开 "公平模式"------ 按排队顺序叫号,哪怕 CEO 来了,也得排在实习生后面。
3. 它咋干活的?(实现原理,还是厕所梗)
① 抢坑(获取锁):裁判的 "一条龙操作"
裁判手里有个小本本(Redis 的 Hash 键),你冲过去要坑位时,他会:
- 查本本:"'VIP 坑位'这栏有人吗?"
- 没人?直接写 "占坑人:张三(线程 ID),次数:1",再贴个 "30 分钟后过期" 的条(PEXPIRE),然后喊 "张三进!",顺便让看门大爷盯着你(启动 Watch Dog)。
- 有人?再看是不是你:是你就把 "次数" 改成 2,续 30 分钟;不是你?就让你站旁边等,还让你关注 "坑位释放群"(Redis Channel),别人一出来就通知你。(全程用 Lua 脚本写在小本本上,保证没人中途改答案 ------ 总不能裁判查一半,有人抢着改本本吧?)
② 蹲坑(持有锁):大爷的 "巡逻任务"
只要你在坑里,看门大爷就每隔 10 秒来续期,直到你出来。要是你突然晕倒在坑里(线程宕机),大爷喊了几声没反应,就不续了 ------30 秒后过期条一到,直接把你 "抬" 出来,给下个人用(避免死锁)。
③ 出坑(释放锁):裁判的 "收尾工作"
你提裤子出来时,裁判会:
- 查本本:"张三,你这次是第 2 次进坑吧?"(确认是锁的持有者)
- 把次数改成 1:"哦?还有 1 次?那你是不是还要进?"(重入次数没到 0,续期继续等)
- 次数到 0 了:直接划掉你的名字(DEL 删除 Hash 键),然后在 "坑位释放群" 里喊 "VIP 坑位空了!下一个来!"(发布订阅通知等待线程),顺便让大爷下班(停止 Watch Dog)。
最后记个 "口诀",保证不忘:
厕所抢位靠 Redis,Redisson 来当机;重入像回家拿纸,续期靠大爷巡逻;抢坑要么死等,要么问完就走;出坑划掉名字,群里喊下一个!
怎么样?下次想到 "蹲坑",就想起 Redisson 了吧~