黑马点评 Redisson 一:为什么手写 Redis 分布式锁之后,还要学习 Redisson?
本文整理自我学习黑马点评 Redis 实战篇第 5 章「分布式锁 Redisson」的 5.1 和 5.2 小节。
第 4 章我们已经用 Redis 的
setIfAbsent、过期时间、线程标识和 Lua 脚本手写过一个分布式锁。刚开始学到第 5 章时,我最大的疑惑是:既然自己已经能写出 Redis 分布式锁了,为什么还要引入 Redisson?它到底是另一个 Redis,还是 Java 里操作 Redis 的一个工具?这篇文章就围绕这个问题展开:Redisson 为什么出现,它替代了我们手写锁的哪一部分,业务代码中
RLock、tryLock()、unlock()到底在配合什么。
1. 为什么这一章不是简单换一个 API
学完前面的手写 Redis 分布式锁后,我们已经知道一把基本的 Redis 锁要处理这些事情:
text
1. 用一个 Redis key 表示锁。
2. 使用 setIfAbsent / SET NX 保证只有一个线程能创建锁。
3. 设置过期时间,防止服务宕机后锁永远不释放。
4. value 中保存线程标识,避免误删别人的锁。
5. 解锁时用 Lua 保证"判断锁归属 + 删除锁"的原子性。
到这里,一个很自然的问题就来了:
既然我们已经把 Redis 分布式锁写出来了,为什么讲义后面还要专门讲 Redisson?
一开始我也容易把 Redisson 理解成"又一种 Redis 用法",好像它只是把 SimpleRedisLock 换成了 RLock。但真正理解后会发现,Redisson 并不是在否定我们前面手写锁的价值。
更准确地说:
第 4 章手写 Redis 分布式锁,是为了理解底层原理;第 5 章学习 Redisson,是为了理解生产中更成熟的分布式锁封装。
手写锁帮我们看清分布式锁的基本骨架。Redisson 则是在这个骨架上继续补齐更多复杂能力,比如可重入、锁重试、自动续期、更多锁类型等。
2. Redisson 到底是什么
先把一个最容易误解的点说清楚:
Redisson 不是 Redis 服务器。
Redis 是服务端中间件,我们的 Java 项目通过网络连接 Redis。Redisson 是 Java 侧的 Redis 客户端框架,它帮我们更方便地使用 Redis 提供的各种能力。
你可以把关系理解成这样:
text
Java 业务代码
↓
Redisson 客户端框架
↓
Redis 服务端
在黑马点评第 5 章里,我们重点关注的是 Redisson 的分布式锁能力。它把很多锁相关的细节封装成了 Java 对象,比如:
java
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
lock.unlock();
表面上看只是几个简单方法,底层其实还是在通过 Redis key、Lua 脚本、过期时间、线程标识等机制完成分布式协调。
所以 Redisson 的定位可以这样概括:
Redisson 是一个基于 Redis 的 Java 客户端框架,它把分布式锁这类复杂能力封装成了更易用的 Java API。
3. 手写锁已经解决了什么,又还缺什么
前面我们自己写的 SimpleRedisLock 已经能解决很多问题。
它大概具备这些能力:
text
1. 能通过 Redis key 抢锁。
2. 能设置锁过期时间,避免死锁。
3. 能保存线程标识,区分锁属于谁。
4. 能通过 Lua 脚本避免误删别人的锁。
这已经比最朴素的 setnx + delete 安全很多了。
但它仍然是一个教学版实现。真实项目里的锁会遇到更多复杂情况,例如:
text
1. 同一个线程重复获取同一把锁怎么办?
2. 抢锁失败后,是不是只能立刻失败?能不能等待一会儿再重试?
3. 锁过期时间怎么设置才合理?业务执行时间超过锁时间怎么办?
4. Redis 主从切换时,锁数据还没同步过去怎么办?
这些就是讲义 5.1 里列出的几个问题:
text
重入问题
不可重试
超时释放
主从一致性
其中 5.1、5.2 只是先告诉我们:这些问题存在,而 Redisson 提供了成熟封装。后面的 5.3、5.4、5.5 才逐步展开其中的原理。
4. Redisson 快速入门第一步:引入依赖
讲义中首先引入 Redisson 依赖:
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
这一步解决的问题是:
让项目能使用 Redisson 提供的 Java API。
没有这个依赖,项目里就没有 RedissonClient、RLock、Config 这些类。
这里要注意,依赖只是把库引进来,还没有真正连接 Redis。连接 Redis 要靠下一步配置。
5. RedissonConfig:告诉 Redisson 去连接哪台 Redis
讲义中的配置类大概是这样:
java
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://<redis-host>:<redis-port>")
.setPassword("你的密码");
return Redisson.create(config);
}
}
这段代码里有几个新东西。
5.1 @Configuration
它是什么:Spring 的配置类注解。
输入是什么:它标在类上,没有普通方法参数。
输出是什么:告诉 Spring 这个类里会定义一些 Bean。
为什么在这里使用:我们要把 RedissonClient 交给 Spring 管理,方便业务类中直接注入。
例子:
java
@Configuration
public class RedissonConfig {
}
可以理解成:
这个类不是普通工具类,而是专门给 Spring 提供配置的。
5.2 @Bean
它是什么:Spring 用来注册对象的方法级注解。
输入是什么:标在一个方法上。
输出是什么:方法返回值会被 Spring 容器管理。
为什么在这里使用:我们希望 Spring 容器里有一个 RedissonClient,后面业务类可以通过 @Resource 注入。
例子:
java
@Bean
public RedissonClient redissonClient() {
return Redisson.create(config);
}
意思是:
Spring,帮我把这个方法返回的
RedissonClient保存起来,后面别人要用时你负责注入。
5.3 Config
它是什么:Redisson 的配置对象。
输入是什么:Redis 地址、密码、连接模式等。
输出是什么:配置本身不直接操作 Redis,而是传给 Redisson.create(config)。
为什么在这里使用:Redisson 不知道 Redis 在哪里,所以必须先配置连接信息。
例子:
java
Config config = new Config();
5.4 useSingleServer()
它是什么:告诉 Redisson 当前使用单 Redis 节点模式。
输入是什么:无。
输出是什么:返回一个单节点配置对象,可以继续链式调用。
为什么在这里使用:讲义当前小节是快速入门,只配置单 Redis 节点。
例子:
java
config.useSingleServer()
这句话可以翻译成:
Redisson,我现在只连接一台 Redis。
5.5 setAddress()
它是什么:设置 Redis 地址。
输入是什么:Redis 连接地址,格式一般是 redis://host:port。
输出是什么:配置对象本身,方便继续链式调用。
为什么在这里使用:Redisson 需要知道 Redis 服务在哪里。
例子:
java
.setAddress("redis://<redis-host>:<redis-port>")
博客里不要写真实 IP、端口和密码,示例中统一用脱敏占位。
5.6 Redisson.create(config)
它是什么:根据配置创建 RedissonClient。
输入是什么:前面配置好的 Config。
输出是什么:RedissonClient。
为什么在这里使用:业务代码不能直接拿 Config 加锁,真正的操作入口是 RedissonClient。
例子:
java
return Redisson.create(config);
一句话理解:
按照这份 Redis 配置,创建一个 Redisson 总入口对象。
6. 业务代码如何使用 Redisson 锁
讲义中的秒杀下单代码从手写锁切换成了 Redisson 锁。
核心代码是:
java
@Resource
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
// 查询秒杀券、判断时间、判断库存 ...
Long userId = UserHolder.getUser().getId();
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
}
这段代码的业务目的和之前的 SimpleRedisLock 一样:
同一个用户的下单临界区,同一时刻只能有一个线程进入。
只是锁的实现从我们自己写,换成了 Redisson 的 RLock。
7. RedissonClient、RLock、getLock()、tryLock() 分别是什么
7.1 RedissonClient
它是什么:Redisson 的总入口对象。
输入是什么:业务使用时不需要自己创建,Spring 注入即可。
输出是什么:可以通过它获取各种 Redisson 对象,比如 RLock。
为什么在这里使用:业务代码需要通过它拿到一把分布式锁。
例子:
java
@Resource
private RedissonClient redissonClient;
可以把它理解成 Redisson 版的"操作入口"。
7.2 getLock(String name)
它是什么:根据锁名称获取一个 RLock 对象。
输入是什么:锁名称,比如 lock:order:10。
输出是什么:RLock。
为什么在这里使用:你必须先拿到锁对象,后面才能调用 tryLock() 和 unlock()。
例子:
java
RLock lock = redissonClient.getLock("lock:order:" + userId);
注意:
getLock()不是加锁成功,它只是拿到锁对象。
真正加锁是下一步。
7.3 RLock
它是什么:Redisson 提供的分布式锁对象。
输入是什么:由 getLock() 返回,业务代码一般不自己 new。
输出是什么:提供 tryLock()、unlock() 等方法。
为什么在这里使用:替代我们自己写的 SimpleRedisLock。
例子:
java
RLock lock = redissonClient.getLock("lock:order:" + userId);
可以粗略理解成:
RLock是 Redisson 给业务代码提供的一把高级分布式锁。
7.4 tryLock()
它是什么:尝试获取锁。
输入是什么:无参版本没有显式等待时间和锁释放时间。
输出是什么:boolean,成功返回 true,失败返回 false。
为什么在这里使用:如果当前用户已经有请求拿到锁,重复请求就不能继续创建订单。
例子:
java
boolean isLock = lock.tryLock();
讲义中还演示了带参数版本:
java
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
它的含义是:
text
最多等待 1 秒去获取锁。
获取锁成功后,锁 10 秒后自动释放。
时间单位是秒。
7.5 unlock()
它是什么:释放锁。
输入是什么:无。
输出是什么:业务通常不关心返回值。
为什么在这里使用:拿到锁后,业务执行完必须释放。
例子:
java
finally {
lock.unlock();
}
为什么要放在 finally?
因为业务代码可能成功,也可能抛异常。只要锁拿到了,就应该尽量释放,避免后续请求一直拿不到锁。
8. Redisson 锁在秒杀下单中的执行流程
讲义当前小节的同步秒杀流程可以画成这样:
#mermaid-svg-PcXhTnSpeiTw1PVc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PcXhTnSpeiTw1PVc .error-icon{fill:#552222;}#mermaid-svg-PcXhTnSpeiTw1PVc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PcXhTnSpeiTw1PVc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PcXhTnSpeiTw1PVc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PcXhTnSpeiTw1PVc .marker.cross{stroke:#333333;}#mermaid-svg-PcXhTnSpeiTw1PVc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PcXhTnSpeiTw1PVc p{margin:0;}#mermaid-svg-PcXhTnSpeiTw1PVc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc .cluster-label text{fill:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc .cluster-label span{color:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc .cluster-label span p{background-color:transparent;}#mermaid-svg-PcXhTnSpeiTw1PVc .label text,#mermaid-svg-PcXhTnSpeiTw1PVc span{fill:#333;color:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc .node rect,#mermaid-svg-PcXhTnSpeiTw1PVc .node circle,#mermaid-svg-PcXhTnSpeiTw1PVc .node ellipse,#mermaid-svg-PcXhTnSpeiTw1PVc .node polygon,#mermaid-svg-PcXhTnSpeiTw1PVc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PcXhTnSpeiTw1PVc .rough-node .label text,#mermaid-svg-PcXhTnSpeiTw1PVc .node .label text,#mermaid-svg-PcXhTnSpeiTw1PVc .image-shape .label,#mermaid-svg-PcXhTnSpeiTw1PVc .icon-shape .label{text-anchor:middle;}#mermaid-svg-PcXhTnSpeiTw1PVc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PcXhTnSpeiTw1PVc .rough-node .label,#mermaid-svg-PcXhTnSpeiTw1PVc .node .label,#mermaid-svg-PcXhTnSpeiTw1PVc .image-shape .label,#mermaid-svg-PcXhTnSpeiTw1PVc .icon-shape .label{text-align:center;}#mermaid-svg-PcXhTnSpeiTw1PVc .node.clickable{cursor:pointer;}#mermaid-svg-PcXhTnSpeiTw1PVc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PcXhTnSpeiTw1PVc .arrowheadPath{fill:#333333;}#mermaid-svg-PcXhTnSpeiTw1PVc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PcXhTnSpeiTw1PVc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PcXhTnSpeiTw1PVc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PcXhTnSpeiTw1PVc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PcXhTnSpeiTw1PVc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PcXhTnSpeiTw1PVc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PcXhTnSpeiTw1PVc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PcXhTnSpeiTw1PVc .cluster text{fill:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc .cluster span{color:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PcXhTnSpeiTw1PVc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PcXhTnSpeiTw1PVc rect.text{fill:none;stroke-width:0;}#mermaid-svg-PcXhTnSpeiTw1PVc .icon-shape,#mermaid-svg-PcXhTnSpeiTw1PVc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PcXhTnSpeiTw1PVc .icon-shape p,#mermaid-svg-PcXhTnSpeiTw1PVc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PcXhTnSpeiTw1PVc .icon-shape .label rect,#mermaid-svg-PcXhTnSpeiTw1PVc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PcXhTnSpeiTw1PVc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PcXhTnSpeiTw1PVc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PcXhTnSpeiTw1PVc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
否
是
用户请求秒杀优惠券
查询秒杀券
是否未开始或已结束?
返回失败
库存是否小于 1?
返回库存不足
获取当前用户 userId
redissonClient.getLock(lock:order:userId)
lock.tryLock()
是否获取锁成功?
返回不允许重复下单
通过代理调用 createVoucherOrder
查订单 + 扣库存 + 创建订单
finally 中 unlock
这张图里最重要的是:
text
Redisson 锁保护的仍然是"一人一单"的临界区。
它不是替代库存乐观锁。库存是否超卖,仍然需要靠:
sql
stock > 0
这种条件更新来保证。
9. 如果不用 Redisson,会怎样
如果继续使用自己手写的 SimpleRedisLock,不是不能跑,而是很多复杂能力需要自己继续补。
比如:
text
1. 可重入需要自己设计数据结构和计数。
2. 锁重试需要自己写等待和唤醒逻辑。
3. 锁自动续期需要自己设计后台续期机制。
4. Redis 多节点可靠性需要自己考虑更多边界。
这些问题不是初学阶段马上都要手写出来,但必须知道它们存在。
所以 Redisson 的价值不是"让代码少几行"这么简单,而是:
它把分布式锁从教学版实现,升级成了更成熟的工程封装。
10. 易错点
易错点一:把 Redisson 当成 Redis 服务器
Redisson 不是 Redis 服务端。它是 Java 客户端框架,底层仍然要连接 Redis。
易错点二:以为 getLock() 就是加锁
getLock() 只是拿锁对象。真正尝试获取锁的是:
java
lock.tryLock();
易错点三:以为 Redisson 锁可以替代库存乐观锁
Redisson 这里锁的是用户维度:
text
lock:order:userId
它防的是同一用户重复下单。
库存是券维度的共享资源,多个不同用户仍然可能同时抢同一张券,所以还需要库存条件更新。
易错点四:忽略 finally 中释放锁
拿到锁后,不管业务成功还是失败,都应该释放锁。否则容易导致其他请求长期拿不到锁。
易错点五:混淆讲义代码和最终版项目代码
讲义 5.2 是同步下单中演示 Redisson 锁。最终版项目可能已经演进到异步下单流程。写博客时应以讲义当前小节为主,最终版项目只作为补充。
11. 面试回答
问:为什么已经手写了 Redis 分布式锁,还要使用 Redisson?
可以这样回答:
手写 Redis 分布式锁可以帮助我们理解底层原理,比如
SET NX抢锁、设置过期时间防死锁、用线程标识防误删、用 Lua 保证解锁原子性。但生产中分布式锁还要考虑可重入、锁重试、自动续期、多种锁类型以及更复杂的 Redis 部署场景。Redisson 是成熟的 Redis Java 客户端框架,它把这些能力封装成RLock等对象,业务代码只需要获取锁、尝试加锁、释放锁即可。
问:RLock lock = redissonClient.getLock(...) 这句是不是已经加锁?
可以这样回答:
不是。
getLock()只是根据锁名称获取一个RLock锁对象,真正尝试获取锁的是tryLock()或lock()方法。只有tryLock()返回true,才表示当前线程获取锁成功。
问:Redisson 锁解决了秒杀中的哪个问题?
可以这样回答:
在这段秒杀业务中,Redisson 锁主要解决一人一单的并发问题,也就是同一个用户的多个并发请求不能同时进入创建订单逻辑。它不直接替代库存乐观锁,因为库存是所有用户共享的券维度资源,仍然需要数据库条件更新来防止超卖。
12. 总结
这一篇主要讲清楚了 Redisson 的入门使用。
第 4 章手写 SimpleRedisLock,重点是理解分布式锁的底层原理。第 5 章引入 Redisson,重点是理解成熟框架如何把这些复杂细节封装起来。
Redisson 不是 Redis 服务器,而是 Java 侧的 Redis 客户端框架。它通过 RedissonClient 提供入口,通过 RLock 提供分布式锁对象。业务代码通过 getLock() 获取锁对象,通过 tryLock() 尝试加锁,通过 unlock() 释放锁。
但要注意,Redisson 锁在秒杀下单中主要保护的是"一人一单"的用户维度临界区。库存超卖问题仍然要靠数据库层面的条件更新来保证。
下一篇继续看 Redisson 的第一个核心能力:可重入锁。也就是为什么同一个线程重复获取同一把锁时,不会把自己锁死。