黑马点评-Redisson-01_why_redisson

黑马点评 Redisson 一:为什么手写 Redis 分布式锁之后,还要学习 Redisson?

本文整理自我学习黑马点评 Redis 实战篇第 5 章「分布式锁 Redisson」的 5.1 和 5.2 小节。

第 4 章我们已经用 Redis 的 setIfAbsent、过期时间、线程标识和 Lua 脚本手写过一个分布式锁。刚开始学到第 5 章时,我最大的疑惑是:既然自己已经能写出 Redis 分布式锁了,为什么还要引入 Redisson?它到底是另一个 Redis,还是 Java 里操作 Redis 的一个工具?

这篇文章就围绕这个问题展开:Redisson 为什么出现,它替代了我们手写锁的哪一部分,业务代码中 RLocktryLock()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。

没有这个依赖,项目里就没有 RedissonClientRLockConfig 这些类。

这里要注意,依赖只是把库引进来,还没有真正连接 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. RedissonClientRLockgetLock()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 的第一个核心能力:可重入锁。也就是为什么同一个线程重复获取同一把锁时,不会把自己锁死。

相关推荐
星轨zb2 小时前
LangChain4j 集成 Spring Boot:会话记忆 NPE 的根源与 ChatMemoryProvider 正确配置
java·spring boot·后端·langchain4j
JAVA9652 小时前
JAVA面试-并发篇 05-并发包AQS队列实现原理是什么
java·开发语言·面试
JAVA面经实录9173 小时前
RocketMQ全套学习知识手册
java·kafka·rabbitmq·rocketmq
phltxy3 小时前
Spring AI 从提示词到多模态
java·人工智能·spring
小满Autumn3 小时前
CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践
笔记·架构·c#·.net·wpf·mvvm
鹿鸣天涯3 小时前
网规第三版:第8章网络故障分析与处理案例
网络·软考·网络规划设计师
Counter-Strike大牛3 小时前
SpringBoot2.7.10+MyBatisPlus实现MySQL+DM双数据库切换
数据库·mysql
上海云盾-小余3 小时前
CN2 与 BGP 线路优劣拆解,按需选配规避延迟与攻击隐患
网络
Halo_tjn3 小时前
反射与设计模式1
java·开发语言·算法