黑马点评 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 锁要处理这些事情:

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 提供的各种能力。

你可以把关系理解成这样:

Java 业务代码
    ↓
Redisson 客户端框架
    ↓
Redis 服务端

在黑马点评第 5 章里,我们重点关注的是 Redisson 的分布式锁能力。它把很多锁相关的细节封装成了 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 已经能解决很多问题。

它大概具备这些能力:

1. 能通过 Redis key 抢锁。
2. 能设置锁过期时间,避免死锁。
3. 能保存线程标识,区分锁属于谁。
4. 能通过 Lua 脚本避免误删别人的锁。

这已经比最朴素的 setnx + delete 安全很多了。

但它仍然是一个教学版实现。真实项目里的锁会遇到更多复杂情况,例如:

1. 同一个线程重复获取同一把锁怎么办?
2. 抢锁失败后,是不是只能立刻失败?能不能等待一会儿再重试?
3. 锁过期时间怎么设置才合理?业务执行时间超过锁时间怎么办?
4. Redis 主从切换时,锁数据还没同步过去怎么办?

这些就是讲义 5.1 里列出的几个问题:

重入问题
不可重试
超时释放
主从一致性

其中 5.1、5.2 只是先告诉我们:这些问题存在,而 Redisson 提供了成熟封装。后面的 5.3、5.4、5.5 才逐步展开其中的原理。


4. Redisson 快速入门第一步:引入依赖

讲义中首先引入 Redisson 依赖:

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

这一步解决的问题是:

让项目能使用 Redisson 提供的 Java API。

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

这里要注意,依赖只是把库引进来,还没有真正连接 Redis。连接 Redis 要靠下一步配置。


5. RedissonConfig:告诉 Redisson 去连接哪台 Redis

讲义中的配置类大概是这样:

@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 管理,方便业务类中直接注入。

例子:

@Configuration
public class RedissonConfig {
}

可以理解成:

这个类不是普通工具类,而是专门给 Spring 提供配置的。

5.2 @Bean

它是什么:Spring 用来注册对象的方法级注解。

输入是什么:标在一个方法上。

输出是什么:方法返回值会被 Spring 容器管理。

为什么在这里使用:我们希望 Spring 容器里有一个 RedissonClient,后面业务类可以通过 @Resource 注入。

例子:

@Bean
public RedissonClient redissonClient() {
    return Redisson.create(config);
}

意思是:

Spring,帮我把这个方法返回的 RedissonClient 保存起来,后面别人要用时你负责注入。

5.3 Config

它是什么:Redisson 的配置对象。

输入是什么:Redis 地址、密码、连接模式等。

输出是什么:配置本身不直接操作 Redis,而是传给 Redisson.create(config)

为什么在这里使用:Redisson 不知道 Redis 在哪里,所以必须先配置连接信息。

例子:

Config config = new Config();

5.4 useSingleServer()

它是什么:告诉 Redisson 当前使用单 Redis 节点模式。

输入是什么:无。

输出是什么:返回一个单节点配置对象,可以继续链式调用。

为什么在这里使用:讲义当前小节是快速入门,只配置单 Redis 节点。

例子:

config.useSingleServer()

这句话可以翻译成:

Redisson,我现在只连接一台 Redis。

5.5 setAddress()

它是什么:设置 Redis 地址。

输入是什么:Redis 连接地址,格式一般是 redis://host:port

输出是什么:配置对象本身,方便继续链式调用。

为什么在这里使用:Redisson 需要知道 Redis 服务在哪里。

例子:

.setAddress("redis://<redis-host>:<redis-port>")

博客里不要写真实 IP、端口和密码,示例中统一用脱敏占位。

5.6 Redisson.create(config)

它是什么:根据配置创建 RedissonClient

输入是什么:前面配置好的 Config

输出是什么:RedissonClient

为什么在这里使用:业务代码不能直接拿 Config 加锁,真正的操作入口是 RedissonClient

例子:

return Redisson.create(config);

一句话理解:

按照这份 Redis 配置,创建一个 Redisson 总入口对象。


6. 业务代码如何使用 Redisson 锁

讲义中的秒杀下单代码从手写锁切换成了 Redisson 锁。

核心代码是:

@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

为什么在这里使用:业务代码需要通过它拿到一把分布式锁。

例子:

@Resource
private RedissonClient redissonClient;

可以把它理解成 Redisson 版的“操作入口”。

7.2 getLock(String name)

它是什么:根据锁名称获取一个 RLock 对象。

输入是什么:锁名称,比如 lock:order:10

输出是什么:RLock

为什么在这里使用:你必须先拿到锁对象,后面才能调用 tryLock()unlock()

例子:

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

注意:

getLock() 不是加锁成功,它只是拿到锁对象。

真正加锁是下一步。

7.3 RLock

它是什么:Redisson 提供的分布式锁对象。

输入是什么:由 getLock() 返回,业务代码一般不自己 new

输出是什么:提供 tryLock()unlock() 等方法。

为什么在这里使用:替代我们自己写的 SimpleRedisLock

例子:

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

可以粗略理解成:

RLock 是 Redisson 给业务代码提供的一把高级分布式锁。

7.4 tryLock()

它是什么:尝试获取锁。

输入是什么:无参版本没有显式等待时间和锁释放时间。

输出是什么:boolean,成功返回 true,失败返回 false

为什么在这里使用:如果当前用户已经有请求拿到锁,重复请求就不能继续创建订单。

例子:

boolean isLock = lock.tryLock();

讲义中还演示了带参数版本:

boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);

它的含义是:

最多等待 1 秒去获取锁。
获取锁成功后,锁 10 秒后自动释放。
时间单位是秒。

7.5 unlock()

它是什么:释放锁。

输入是什么:无。

输出是什么:业务通常不关心返回值。

为什么在这里使用:拿到锁后,业务执行完必须释放。

例子:

finally {
    lock.unlock();
}

为什么要放在 finally

因为业务代码可能成功,也可能抛异常。只要锁拿到了,就应该尽量释放,避免后续请求一直拿不到锁。


8. Redisson 锁在秒杀下单中的执行流程

讲义当前小节的同步秒杀流程可以画成这样:

用户请求秒杀优惠券

查询秒杀券

是否未开始或已结束?

返回失败

库存是否小于 1?

返回库存不足

获取当前用户 userId

redissonClient.getLock(lock:order:userId)

lock.tryLock()

是否获取锁成功?

返回不允许重复下单

通过代理调用 createVoucherOrder

查订单 + 扣库存 + 创建订单

finally 中 unlock

这张图里最重要的是:

Redisson 锁保护的仍然是“一人一单”的临界区。

它不是替代库存乐观锁。库存是否超卖,仍然需要靠:

stock > 0

这种条件更新来保证。


9. 如果不用 Redisson,会怎样

如果继续使用自己手写的 SimpleRedisLock,不是不能跑,而是很多复杂能力需要自己继续补。

比如:

1. 可重入需要自己设计数据结构和计数。
2. 锁重试需要自己写等待和唤醒逻辑。
3. 锁自动续期需要自己设计后台续期机制。
4. Redis 多节点可靠性需要自己考虑更多边界。

这些问题不是初学阶段马上都要手写出来,但必须知道它们存在。

所以 Redisson 的价值不是“让代码少几行”这么简单,而是:

它把分布式锁从教学版实现,升级成了更成熟的工程封装。


10. 易错点

易错点一:把 Redisson 当成 Redis 服务器

Redisson 不是 Redis 服务端。它是 Java 客户端框架,底层仍然要连接 Redis。

易错点二:以为 getLock() 就是加锁

getLock() 只是拿锁对象。真正尝试获取锁的是:

lock.tryLock();

易错点三:以为 Redisson 锁可以替代库存乐观锁

Redisson 这里锁的是用户维度:

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 的第一个核心能力:可重入锁。也就是为什么同一个线程重复获取同一把锁时,不会把自己锁死。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐