RedisTemplate 和 RedissonClient 的区别
(1)定位不同:
- RedisTemplate 是 Spring 提供的工具类,直接对 Redis 的基本数据结构进行操作,适合实现缓存、基本的键值存取功能。
- RedissonClient 是基于 Redis 的高级封装工具,专注于分布式环境下的高级功能,如分布式锁、队列、限流器、集合等。
(2)使用场景不同:
- RedisTemplate 适用于简单的缓存场景,如增删改查键值对、管理 Redis 原生的数据结构(如 List、Set、Hash)。
- RedissonClient 适用于分布式系统场景,如控制资源争抢的分布式锁、任务调度的延迟队列、限流控制等。
(3)开发难度不同:
- RedisTemplate 提供较底层的 API,使用时需要开发者实现复杂逻辑(如分布式锁的实现)。
- RedissonClient 提供更高级的封装和工具类,开发者可以直接使用其内置功能,开发难度更低。
(4)高级功能支持不同:
- RedisTemplate 仅支持 Redis 的基本功能,分布式锁、限流、延迟队列等需要自行开发。
- RedissonClient 内置高级功能,支持分布式锁、阻塞队列、布隆过滤器等,提供丰富的高级 API。
(5)学习曲线不同:
- RedisTemplate 偏向底层,需要开发者了解 Redis 的数据结构和命令。
- RedissonClient 高度封装,API 类似于 Java 的集合,易学易用。
特性 | RedisTemplate | RedissonClient |
---|---|---|
定位 | Redis 的底层操作工具 | 高级封装的分布式工具 |
适用场景 | 简单的缓存、基础的键值操作 | 分布式锁、队列、限流、集合等复杂分布式场景 |
功能范围 | 支持 String、Hash、List、Set 等基础数据结构操作 | 提供分布式锁、延迟队列、高级集合、限流等功能 |
分布式锁 | 需要手动实现,逻辑复杂 | 内置分布式锁支持,自动续约 |
分布式队列 | 支持 Redis List 的基础队列操作 | 支持阻塞队列、延迟队列 |
限流 | 需自行设计限流逻辑 | 内置限流器(如 RRateLimiter ) |
编程风格 | 偏底层,操作 Redis 命令 | 高度封装,面向对象的使用方式 |
扩展性 | 支持基础数据结构,扩展功能需自行开发 | 提供丰富的分布式数据结构和服务 |
部署模式支持 | 支持单机、主从、哨兵、集群模式 | 支持单机、主从、哨兵、集群模式 |
性能 | 操作简单数据时性能较高 | 高级功能增加了额外的网络开销,性能略低 |
学习曲线 | 熟悉 Redis 操作命令后可快速上手 | 更易上手,但需要学习高级功能的配置和使用 |
第一部分:RedissonClient 基础知识
1. Redisson 简介
Redisson 是什么?
Redisson 是基于 Redis 的 Java 客户端,扩展了 Redis 的基础功能,提供了分布式锁、分布式集合、队列、信号量、限流器等高级功能。它不仅可以作为一个 Redis 客户端进行普通的 Redis 数据操作,还特别适用于分布式系统中需要高可用性、高并发控制和多线程支持的场景。
特点:
-
基于 Redis:Redisson 作为一个高级的 Redis 客户端,它使用 Redis 作为底层存储,通过 Redis 提供的丰富数据结构和功能实现分布式特性。
-
支持高级功能:Redisson 增强了 Redis 的基本数据结构,提供了更多分布式功能,比如分布式锁、队列、集合、信号量、布隆过滤器、限流器等。
-
异步和反应式支持:Redisson 支持异步和反应式编程模型,能与 Java 8 的
CompletableFuture
或 Spring 的反应式框架无缝结合。
Redisson 与 Redis 的关系
Redisson 是基于 Redis 的客户端,它利用 Redis 提供的底层存储功能,扩展并实现了高级功能。尽管 Redisson 使用 Redis 作为底层数据存储,但它的功能超出了基本的 Redis 数据操作,包括对分布式系统中的共享资源的管理和控制。
关系概述:
-
Redis:是一个开源的内存数据库,提供基础的数据存储和缓存能力。
-
Redisson:是一个 Java 客户端,它在 Redis 的基础上,提供了分布式锁、集群支持、队列、集合等高级功能,使得开发者可以更容易地实现分布式应用。
Redisson 的核心功能
Redisson 提供了以下几种核心功能,帮助开发者在分布式环境中处理共享资源、锁、缓存等问题:
-
分布式锁:Redisson 提供了可重入锁、公平锁、读写锁等多种分布式锁,可以用于分布式环境中的并发控制,防止数据不一致的问题。
-
分布式集合:Redisson 提供了分布式数据结构,如分布式 Map、Set、List、Queue 等,可以处理分布式环境中的数据存储、任务队列等问题。
-
分布式队列:Redisson 提供了分布式队列和阻塞队列,适用于需要跨多个节点处理任务的场景。
-
异步编程:支持异步 API,使得操作 Redis 数据时可以不阻塞主线程,提高系统吞吐量。
-
多种 Redis 部署模式:支持 Redis 单机、主从、哨兵、集群模式,适用于不同规模的 Redis 部署。
RedissonClient 的适用场景
Redisson 提供了丰富的功能,适用于以下场景:
-
分布式锁控制:
-
在分布式系统中,需要确保某个操作在同一时刻只被一个节点执行,Redisson 提供的分布式锁(如
RLock
)可以有效解决这个问题。 -
示例:使用
RLock
来确保秒杀系统中同一商品的库存操作不会同时被多个请求修改,避免超卖。
-
-
缓存优化:
-
Redisson 可以作为缓存工具,结合 Redis 的高效存储能力,在高并发场景下提供高速的数据访问。使用 Redisson 的分布式集合、Map 等数据结构,可以轻松实现分布式缓存系统。
-
示例:可以使用 Redisson 中的分布式 Map 来实现多节点缓存共享。
-
-
数据一致性管理:
-
在多节点的 Redis 集群环境中,数据可能会在多个节点之间分片存储。Redisson 提供的数据结构支持跨节点的一致性读写。
-
示例:通过 Redisson 提供的分布式队列来确保任务的顺序执行,并且在不同节点之间保持一致性。
-
2. 环境搭建
引入 Maven 依赖
在 Spring Boot 项目中,要使用 Redisson 客户端,你需要通过 Maven 来引入 redisson-spring-boot-starter
依赖。以下是添加依赖的步骤:
-
打开
pom.xml
文件。 -
在
<dependencies>
标签中添加 Redisson 依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.21.3</version>
</dependency>
-
redisson-spring-boot-starter
:这是 Redisson 官方提供的 Spring Boot 自动配置包,它将 Redisson 的所有功能集成到 Spring Boot 项目中,使得配置和使用更加简单。
测试连接是否正常
完成 Maven 依赖的配置后,你可以测试 Redisson 是否正确连接到 Redis 实例。通过注入 RedissonClient
并调用其 getConfig()
方法,可以验证连接是否成功。
@Autowired
private RedissonClient redissonClient;
public void test() {
System.out.println("连接成功:" + redissonClient.getConfig());
}
-
RedissonClient
:这是 Redisson 提供的客户端接口,它允许你与 Redis 交互并执行分布式操作。 -
getConfig()
:通过调用redissonClient.getConfig()
,可以获取当前 Redisson 配置的详细信息。若 Redis 连接成功,则会输出相关的配置信息。
Redisson 提供了比 RedisTemplate 更强大的功能,特别是在分布式系统中处理共享资源时,能够有效提高系统的稳定性和可靠性。
第二部分:分布式锁
1. RedissonClient分布式锁的底层原理
Redisson 的分布式锁主要基于 Redis 的 SETNX(Set if Not Exists)命令(用于加锁,该命令本身原子性)和 Lua 脚本(用于解锁)来实现的。
(1)加锁阶段(可重入锁)
使用 Redis 的 SETNX
命令尝试写入一个唯一标识的锁。如果 SETNX
成功(返回 1),说明锁被成功获取。如果 SETNX
失败(返回 0),说明锁已被其他线程持有。
生成唯一标识
生成客户端标识 (UUID)
每个客户端节点在执行分布式锁操作时,Redisson 会为它生成一个唯一的 UUID。
UUID 通常由
UUID.randomUUID().toString()
生成,确保全球唯一。UUID 的作用是确保即使同一服务启动多次,每个实例的锁标识也不会重复。
生成线程标识 (ThreadId)
在同一客户端节点中,Redisson 还会为每个加锁的线程生成一个唯一的 ThreadId。
ThreadId 主要用于实现 可重入锁,防止同一线程多次加锁时误释放其他线程的锁。
每个客户端节点就是一个一个 Spring Boot 服务实例
+-----------------------+ +-----------------------+ | Spring Boot 实例 1 | | Spring Boot 实例 2 | | 订单服务(客户端) | | 订单服务(客户端) | +-----------------------+ +-----------------------+ | | | | +----------------------------+----------------------------+ | Redis | | (分布式锁存储) | +---------------------------------------------------------+
第一次加锁:
- 使用
SET resource_lock <UUID:ThreadId> NX PX 10000
创建锁。 - 在 Redis 中写入锁标识,成功后表示当前线程已获得锁。
SET resource_lock <UUID:ThreadId> NX PX 10000
-
SET:设置一个 Key-Value 对。
-
resource_lock:锁的 key,通常是基于业务场景生成,例如
order_lock:123
. -
UUID:ThreadId:锁的 Value,包含客户端的唯一标识和线程 ID。
-
NX:
SETNX
的作用,如果 key 不存在则创建。 -
PX 10000:设置锁的有效期,10 秒为示例,实际可配置。
重入加锁(同一线程再次加锁):
当同一线程再次加锁时(重入场景),Redisson 会检测到当前线程已经持有了锁。此时它不会再执行 SETNX
,而是直接基于 Redis 的 Hash 结构 来存储和维护重入次数。每重入一次Count
就加1
HSET resource_lock:count <UUID:ThreadId> 2
释放锁时:
HINCRBY resource_lock:count <UUID:ThreadId> -1
-
Redisson 会先检查
Count
值。 -
如果
Count > 1
,则执行HINCRBY
将Count
减 1: -
如果
Count = 0
,则释放锁,执行DEL resource_lock
。
(2)自动续期机制(看门狗机制)
启动看门狗线程:
当线程成功加锁后,Redisson 会在客户端启动一个看门狗线程。
自动续期:
每隔一段时间(默认为 30 秒),看门狗会检查线程的存活状态。
如果线程还在运行,则执行以下 Redis 命令,将锁的过期时间延长:
PEXPIRE resource_lock 10000
作用:将锁的过期时间重置为 10 秒,确保任务没有被意外中断。
线程异常处理:
-
如果线程崩溃或宕机,Redis 将在锁过期后自动释放锁。
-
看门狗不会再为它续期,其他线程可以重新获取锁。
(3)锁的等待和重试机制
当线程尝试获取锁失败时,Redisson 提供了两种等待方式:
自旋重试(lock.tryLock())
如果使用 tryLock()
并设置了等待时间,线程会在一段时间内不断轮询 Redis 是否有锁释放。
阻塞等待(lock.lock())
使用 lock()
方法时,Redisson 会阻塞当前线程,直到获取到锁。
内部机制:
-
线程会进入 等待队列,通过 Redis 的
pub/sub
(发布订阅)机制监听锁的释放事件。 -
锁释放后,等待中的线程会被通知,重新尝试获取锁。
(4)解锁阶段
-
释放锁时,会使用 Lua 脚本进行原子判断和删除。
-
Lua 脚本确保只有持有锁的线程才能删除锁,避免误删。
// 解锁(非原子操作)
if (redis.get("lock").equals(lockValue)) {
redis.del("lock"); // 🔥 如果在这里之前锁过期,另一个线程获得锁,就会被误删
}
这个判断和删除不是原子操作!中间可能被其他线程插入,导致:
-
线程 A 获得锁后卡顿,锁自动过期
-
线程 B 获得了新的锁
-
线程 A 恢复继续执行,发现 key 还存在,并执行
DEL
—— 误删了线程 B 的锁!
典型的解锁 Lua 脚本如下:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
-
KEYS[1]
:锁的 key(例如"lock:order:123"
) -
ARGV[1]
:当前线程保存的唯一标识值(加锁时设置的 value) -
脚本执行流程:
-
如果 key 的 value 与我们提供的相同(我们持有锁),就删掉这个锁
-
否则不做任何操作
-
2. 获取锁的基本 API
具体使用格式
String lockkey = String.format(SystemConstants.LOCK_KEY, key);
RLock rlock = redissonClient.getLock(lockkey);
try {
boolean acquired = rlock.tryLock(300, 1000, TimeUnit.MILLISECONDS);
if (acquired) {
try {
//数据库查询并写入缓存
} finally {
rlock.unlock();
}
//如果没有获取到锁,就轮询查看缓存是否已经被写入
} else {
for (int i = 0; i < 3; i++) {
//查找缓存,如果缓存不为空,则直接获取
Thread.sleep(300);
}
//如果轮询完缓存依旧没有补充,抛出错误
throw new BaseException(ResponseCodeEnum.);
}
//下面就是对于trylock方法可能被中断抛出的异常的捕获
} catch (InterruptedException e) {
// 1. 恢复中断状态(重要!)
Thread.currentThread().interrupt();
throw new BaseException(ResponseCodeEnum.);
}
(1) 获取锁实例
RLock lock = redissonClient.getLock("lockName");
getLock("lockName")
:获取一个分布式锁实例,lockName
是 Redis 中的 key,表示该锁的唯一标识。- 返回值:
RLock
是 Redisson 提供的分布式锁接口,可以对该锁进行加锁和解锁操作。
(2) 阻塞式加锁
lock.lock();
- 描述:阻塞式加锁方法。如果锁被其他线程占用,则当前线程会进入阻塞状态,直到获取到锁。
- 默认锁超时时间:30 秒(可以通过配置修改)。
示例:
RLock lock = redissonClient.getLock("myLock");
try {
lock.lock(); // 阻塞直到获取到锁
System.out.println("成功获取锁,执行业务逻辑");
} finally {
lock.unlock(); // 确保释放锁
}
(3) 带超时时间的加锁
lock.lock(10, TimeUnit.SECONDS);
- 描述:加锁方法,指定锁的持有时间。如果超过指定时间,锁会自动释放。
- 参数说明:
10
:锁的持有时间。TimeUnit.SECONDS
:时间单位(秒)。
示例:
RLock lock = redissonClient.getLock("myLock");
try {
lock.lock(10, TimeUnit.SECONDS); // 持有锁 10 秒
System.out.println("锁定成功,执行业务逻辑");
} finally {
lock.unlock(); // 手动释放锁
}
(4) 尝试加锁
boolean isLocked = lock.tryLock(5, 30, TimeUnit.SECONDS);
- 描述:尝试获取锁,在指定时间内等待。如果获取到锁,锁的持有时间为指定的时间。
- 参数说明:
5
:最大等待时间。30
:锁的持有时间。TimeUnit.SECONDS
:时间单位。
- 返回值:布尔值,
true
表示成功获取锁,false
表示等待时间内未能获取锁。
示例:
RLock lock = redissonClient.getLock("myLock");
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 等待 5 秒,持有锁 30 秒
System.out.println("成功获取锁,执行业务逻辑");
} else {
System.out.println("获取锁失败");
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 确保当前线程持有锁时释放锁
}
}
(5) 判断锁是否被占用
boolean isLocked = lock.isLocked();
- 描述:检查当前锁是否被任何线程占用。
- 返回值:
true
:锁已被占用。false
:锁未被占用。
示例:
RLock lock = redissonClient.getLock("myLock");
if (lock.isLocked()) {
System.out.println("锁已被占用");
} else {
System.out.println("锁未被占用");
}
(6) 判断当前线程是否持有锁
boolean isHeld = lock.isHeldByCurrentThread();
- 描述:检查当前线程是否持有该锁。
- 返回值:
true
:当前线程持有锁。false
:当前线程未持有锁。
示例:
RLock lock = redissonClient.getLock("myLock");
if (lock.isHeldByCurrentThread()) {
System.out.println("当前线程持有锁");
} else {
System.out.println("当前线程未持有锁");
}
(7) 解锁
lock.unlock();
- 描述:释放锁,确保在业务逻辑完成后调用此方法释放锁。
- 注意:
- 只有持有锁的线程可以解锁。
- 如果尝试释放未持有的锁,会抛出异常。
示例:
RLock lock = redissonClient.getLock("myLock");
try {
lock.lock();
// 执行业务逻辑
} finally {
lock.unlock(); // 确保释放锁
}
(8)自动续约机制(看门狗机制)
看门狗机制的触发条件:看门狗机制仅在以下情况下生效:
-
你调用的是无参的
lock()
方法,例如:RLock lock = redisson.getLock("lock_key"); lock.lock(); // 看门狗会生效
-
或者你调用的是
tryLock()
方法且未指定leaseTime
,例如:RLock lock = redisson.getLock("lock_key"); lock.tryLock(10, TimeUnit.SECONDS); // 看门狗会生效
如何避免看门狗机制延长锁的时间?
如果你希望锁的超时时间固定,且不希望看门狗机制介入,可以采取以下措施:
-
显式设置
leaseTime
:在加锁时指定leaseTime
,例如:这样,锁的超时时间会被固定为 30 秒,不会自动续期。这种方式同样会固定锁的超时时间,看门狗不会生效。
RLock lock = redisson.getLock("lock_key");
lock.lock(30, TimeUnit.SECONDS); // 锁的超时时间为 30 秒,看门狗不会生效
-
使用
tryLock(waitTime, leaseTime, unit)
:如果你希望在获取锁时设置等待时间和锁的超时时间,可以使用tryLock
:
RLock lock = redisson.getLock("lock_key");
boolean res = lock.tryLock(10, 30, TimeUnit.SECONDS); // 等待 10 秒,锁超时 30 秒
if (res) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
log.warn("获取锁失败");
}
-
禁用看门狗机制:如果你希望全局禁用看门狗机制,可以通过 Redisson 的配置实现:这样,即使调用无参的
lock()
方法,看门狗也不会生效。
Config config = new Config();
config.setLockWatchdogTimeout(0); // 禁用看门狗
RedissonClient redisson = Redisson.create(config);
看门狗机制默认启用:
如果你直接调用lock()方法,不加超时时间,那么即使你没有在redission的bean的配置中设置setLockWatchdogTimeout,redission也会默认启用看门狗机制(默认的配置是30秒)
Config config = new Config();
config.setLockWatchdogTimeout(60000L); // 设置看门狗的超时时间为 60 秒
RedissonClient redisson = Redisson.create(config);
3. Redisson 提供的四种分布式锁类型
(1) 可重入锁(Reentrant Lock)
特点:
- 同一线程可以多次获取同一锁。
- 锁内部有一个计数器记录锁的重入次数,只有在完全释放锁后,其他线程才能获取该锁。
适用场景:
- 普通的线程互斥控制。
- 涉及递归调用的场景(同一线程需要多次加锁)。
使用示例:
RLock lock = redissonClient.getLock("reentrantLock");
try {
// 第一次加锁
lock.lock();
// 递归调用时再次加锁(可重入)
lock.lock();
// 业务逻辑
} finally {
// 解锁两次,完全释放锁
lock.unlock();
lock.unlock();
}
适用场景举例:
- 某方法需要递归调用自身,每次递归时需要确保线程安全。
(2) 公平锁(Fair Lock)
特点:
- 公平锁按照线程请求锁的顺序授予锁。
- 在锁竞争激烈的场景中,确保不会发生“锁饥饿”(某些线程始终无法获取锁)。
RLock lock = redissonClient.getFairLock("fairLock");
适用场景:
- 多线程竞争激烈且需要严格按照顺序获取锁的场景。
使用示例:
RLock lock = redissonClient.getFairLock("fairLock");
try {
lock.lock();
// 业务逻辑
} finally {
lock.unlock();
}
适用场景举例:
- 高并发的队列任务分配,确保任务处理按照顺序进行。
与可重入锁的区别:
- 可重入锁不保证线程获取锁的顺序,而公平锁严格按照请求顺序分配锁。
(3) 读写锁(ReadWrite Lock)
特点:
- 提供两种锁:读锁(共享锁)和写锁(独占锁)。
- 读锁:允许多个线程同时读取资源。
- 写锁:只允许一个线程写入资源,且写锁会阻塞其他线程的读写操作。
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock");
适用场景:
- 读多写少的场景,如缓存数据更新。
使用示例:
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock");
// 获取读锁(共享锁)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 读取操作(可同时被多个线程执行)
} finally {
readLock.unlock();
}
// 获取写锁(独占锁)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 写入操作(会阻塞其他读写操作)
} finally {
writeLock.unlock();
}
适用场景举例:
- 商品详情页面:
- 多个线程可以同时读取商品信息(读锁)。
- 当商品信息需要更新时,写操作需要阻塞所有读取操作(写锁)。
与其他锁的区别:
- 可重入锁和公平锁都是独占锁,而读写锁提供了共享访问机制。
(4) 红锁(Red Lock)
特点:
- 在多个 Redis 实例上同时加锁,提高分布式锁的可靠性。
- 锁只有在大多数 Redis 节点上成功加锁后才算获取成功。
适用场景:
- 高可用的 Redis 集群环境,尤其是需要跨多个数据中心部署 Redis 的场景。
使用示例:
RLock lock = redissonClient.getLock("redLock");
try {
lock.lock();
// 业务逻辑
} finally {
lock.unlock();
}
适用场景举例:
- 分布式系统中多个节点需要高可用的锁机制,例如跨区域数据同步。
与其他锁的区别:
- 可重入锁、公平锁、读写锁通常依赖单个 Redis 实例,而红锁在多个 Redis 节点上实现分布式加锁,提高可靠性。
四种锁的对比总结
锁类型 | 特点 | 适用场景 |
---|---|---|
可重入锁 | 同一线程可以多次获取锁,只有完全释放锁后其他线程才能获取。 | 普通互斥场景,递归调用时需要多次加锁。 |
公平锁 | 严格按照线程请求锁的顺序分配,避免锁饥饿。 | 任务调度、队列分配等需要严格顺序控制的场景。 |
读写锁 | 提供读锁(共享锁)和写锁(独占锁),读写操作互斥。 | 读多写少的场景,如缓存数据更新。 |
红锁 | 在多个 Redis 节点上加锁,大多数节点加锁成功则获取锁。 | 高可用、高可靠的分布式锁需求,例如跨数据中心的任务分配。 |
4. 使用示例
防止库存超卖
分布式锁可确保在多线程或多实例环境下,库存的扣减逻辑不会出现超卖。
RLock lock = redissonClient.getLock("stockLock");
try {
if (lock.tryLock(10, 60, TimeUnit.SECONDS)) {
// 执行业务逻辑:扣减库存
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
stock--;
redisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("库存不足");
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
说明:
tryLock(10, 60, TimeUnit.SECONDS)
:确保锁只持有 60 秒,防止死锁。- 确保在
finally
块中释放锁,避免死锁。
幂等性任务控制
分布式锁可用于确保任务在分布式环境中仅被执行一次,例如处理重复的支付请求。
RLock lock = redissonClient.getLock("taskLock");
try {
if (lock.tryLock(10, 60, TimeUnit.SECONDS)) {
// 执行业务逻辑:确保任务只执行一次
String taskStatus = redisTemplate.opsForValue().get("taskStatus");
if ("DONE".equals(taskStatus)) {
System.out.println("任务已完成,无需重复执行");
} else {
// 执行任务
System.out.println("执行任务中...");
redisTemplate.opsForValue().set("taskStatus", "DONE");
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
第三部分:分布式集合
1. 分布式集合的基础知识
分布式集合是 Redisson 提供的一组分布式数据结构,它们基于 Redis 实现,允许在分布式环境下使用类似于 Java 集合框架的操作方式。这些分布式集合具有线程安全性和高性能特点,适合高并发场景中的多线程或多实例操作。
1.1 分布式集合在高并发场景的应用
-
数据缓存
- 使用分布式
RMap
(分布式哈希表)可以实现缓存系统。 - 数据可以通过设置过期时间自动失效,避免占用过多内存。
- 使用分布式
-
分布式任务队列
- 使用分布式
RQueue
和RBlockingQueue
可以实现分布式任务调度。 - 消费者和生产者可以在不同的线程或服务实例中协同工作。
- 使用分布式
-
数据去重
- 使用分布式
RSet
(分布式集合)可以实现数据去重。 - 适合存储不重复的元素,如唯一的用户 ID、产品 SKU 等。
- 使用分布式
2. 常用分布式集合
2.1 RMap(分布式哈希表)
- 用途:存储键值对,支持过期时间。
- 特点:
- 类似于 Java 的
HashMap
,但支持分布式和线程安全。 - 可以设置每个键值对的单独过期时间。
- 类似于 Java 的
基本操作示例:
RMap<String, String> map = redissonClient.getMap("myMap");
// 插入键值对
map.put("key", "value");
// 获取值
String value = map.get("key");
// 删除键值对
map.remove("key");
基本操作
V put(K key, V value)
插入或更新键值对。V putIfAbsent(K key, V value)
仅在键不存在时插入键值对。boolean remove(Object key, Object value)
删除指定键值对。V remove(Object key)
删除指定键并返回其值。V get(Object key)
获取指定键的值。boolean containsKey(Object key)
判断键是否存在。boolean containsValue(Object value)
判断值是否存在。int size()
获取键值对的数量。boolean isEmpty()
判断是否为空。Set<K> keySet()
获取所有键。Collection<V> values()
获取所有值。Set<Map.Entry<K, V>> entrySet()
获取键值对集合。void clear()
清空哈希表。boolean replace(K key, V oldValue, V newValue)
替换指定键的值。V replace(K key, V value)
替换指定键的值并返回旧值。void putAll(Map<? extends K, ? extends V> map)
批量插入键值对。
过期时间操作
void put(K key, V value, long ttl, TimeUnit unit)
设置键值对及其过期时间。boolean expireKey(K key, long timeToLive, TimeUnit timeUnit)
设置指定键的过期时间。boolean clearExpire(K key)
清除指定键的过期时间。long remainTimeToLive(K key)
获取指定键的剩余存活时间。
2.2 RList(分布式列表)
- 用途:存储有序数据。
- 特点:
- 类似于 Java 的
ArrayList
,支持按索引访问。 - 适合实现分布式任务队列或记录操作日志。
- 类似于 Java 的
基本操作示例:
RList<String> list = redissonClient.getList("myList");
// 添加元素
list.add("value1");
list.add("value2");
// 获取元素
String value = list.get(0);
// 删除元素
list.remove("value1");
适用场景:
- 任务调度:将任务添加到列表中,按顺序执行。
- 日志记录:记录用户操作或系统日志。
基本操作
boolean add(E e)
添加元素到列表尾部。void add(int index, E element)
在指定位置插入元素。boolean addAll(Collection<? extends E> c)
添加一个集合到列表尾部。boolean addAll(int index, Collection<? extends E> c)
在指定位置插入一个集合。E get(int index)
获取指定位置的元素。int size()
获取列表长度。boolean isEmpty()
判断是否为空。boolean contains(Object o)
判断是否包含某元素。int indexOf(Object o)
获取元素的第一个索引。int lastIndexOf(Object o)
获取元素的最后一个索引。E remove(int index)
删除指定位置的元素并返回。boolean remove(Object o)
删除指定元素。void clear()
清空列表。
高级操作
List<E> subList(int fromIndex, int toIndex)
获取子列表。void trim(int fromIndex, int toIndex)
保留指定范围的元素,其余元素将被删除。E set(int index, E element)
替换指定位置的元素。Iterator<E> iterator()
返回迭代器。void sort(Comparator<? super E> c)
对列表进行排序。void fastRemove(int index)
快速删除指定索引的元素。
2.3 RSet(分布式集合)
- 用途:存储唯一元素,用于去重场景。
- 特点:
- 类似于 Java 的
HashSet
,支持快速判断元素是否存在。 - 适合存储不重复的用户 ID、订单号等。
- 类似于 Java 的
基本操作示例:
RSet<String> set = redissonClient.getSet("mySet");
// 添加元素
set.add("value1");
set.add("value2");
// 检查元素是否存在
boolean exists = set.contains("value1");
// 删除元素
set.remove("value1");
适用场景:
- 数据去重:确保集合中的元素唯一。
- 用户访问记录:记录用户访问的页面或操作。
基本操作
boolean add(E e)
添加元素到集合。boolean remove(Object o)
删除指定元素。int size()
获取集合的大小。boolean isEmpty()
判断集合是否为空。boolean contains(Object o)
判断集合是否包含指定元素。void clear()
清空集合。boolean addAll(Collection<? extends E> c)
添加所有元素到集合。boolean removeAll(Collection<?> c)
删除集合中的所有指定元素。boolean retainAll(Collection<?> c)
仅保留集合中与指定集合共有的元素。Iterator<E> iterator()
获取迭代器。
高级操作
boolean move(String destinationSetName, E value)
将元素移动到另一个集合。Set<E> readAll()
读取所有元素。int union(String... setNames)
计算集合的并集。int diff(String... setNames)
计算集合的差集。int intersect(String... setNames)
计算集合的交集。
3. 使用示例
3.1 使用分布式 Map 实现缓存
分布式 RMap
可以用来实现缓存系统,支持为每个键值对设置独立的过期时间。
示例代码:
RMap<String, String> cache = redissonClient.getMap("cache");
// 添加缓存数据,并设置过期时间为 10 分钟
cache.put("key", "value", 10, TimeUnit.MINUTES);
// 获取缓存数据
String value = cache.get("key");
// 删除缓存数据
cache.remove("key");
说明:
- 键值对的过期时间:
put("key", "value", 10, TimeUnit.MINUTES)
表示该键值对将在 10 分钟后失效。 - 线程安全性:即使在多个线程或实例中访问同一个
RMap
,数据操作也是安全的。
3.2 使用分布式 List 实现任务队列
分布式 RList
可以用于实现任务队列,将任务存储到列表中,消费者线程按顺序取出任务。
示例代码:
RList<String> taskQueue = redissonClient.getList("taskQueue");
// 生产者线程:添加任务
taskQueue.add("task1");
taskQueue.add("task2");
// 消费者线程:获取任务并处理
while (!taskQueue.isEmpty()) {
String task = taskQueue.remove(0);
System.out.println("处理任务:" + task);
}
3.3 使用分布式 Set 实现去重
分布式 RSet
适合用于去重场景,例如记录唯一的访问用户 ID。
示例代码:
RSet<String> uniqueUsers = redissonClient.getSet("uniqueUsers");
// 添加用户 ID
uniqueUsers.add("user1");
uniqueUsers.add("user2");
// 检查用户是否已访问
if (uniqueUsers.contains("user1")) {
System.out.println("用户 user1 已访问");
}
// 删除用户 ID
uniqueUsers.remove("user2");
分布式集合 | 用途 | 特点 | 适用场景 |
---|---|---|---|
RMap | 分布式哈希表 | 存储键值对,支持过期时间 | 数据缓存 |
RList | 分布式列表 | 存储有序数据,支持按索引访问 | 任务队列、日志记录 |
RSet | 分布式集合 | 存储唯一元素,快速去重 | 数据去重、唯一用户访问记录 |
分布式集合的核心优势:
- 线程安全:在多线程或分布式环境中使用数据结构,确保数据操作安全。
- 高性能:基于 Redis 的高性能存储和操作。
- 易用性:API 接近 Java 原生集合,学习成本低。
第四部分:分布式服务
分布式队列是 Redisson 提供的一个重要功能,它允许在分布式环境中处理任务队列,用于实现生产者-消费者模型和延迟任务调度。以下介绍两种常见队列类型:
1. 普通队列(RQueue)
-
特点:
- 类似于 Java 的
Queue
,支持 FIFO(先进先出)操作。 - 适用于简单的任务队列场景,例如生产者添加任务,消费者按顺序取出任务处理。
- 类似于 Java 的
-
使用场景:
- 任务处理:将任务添加到队列中,消费者线程从队列取出任务并执行。
- 数据流:记录系统中的数据流事件,按顺序处理。
基本操作示例:
RQueue<String> queue = redissonClient.getQueue("myQueue");
// 添加任务到队列
queue.add("task1");
queue.add("task2");
// 按顺序取出任务
String task = queue.poll(); // 返回并移除队首任务
System.out.println("处理任务:" + task);
2. 延迟队列(RDelayedQueue)
-
特点:
- 延迟队列可以设置任务的延迟时间,只有在延迟时间到期后,任务才会进入目标队列。
- 适用于需要延迟处理的场景,例如定时任务、订单超时检查。
-
使用场景:
- 延迟任务调度:在指定时间后执行任务。
- 订单超时处理:在未支付订单超时时自动取消订单。
- 定时事件提醒:如会议开始前 10 分钟的提醒通知。
基本操作示例:
// 获取目标队列
RQueue<String> queue = redissonClient.getQueue("myQueue");
// 创建延迟队列
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);
// 添加任务到延迟队列,设置延迟时间为 10 秒
delayedQueue.offer("task", 10, TimeUnit.SECONDS);
// 模拟取出任务
while (true) {
String task = queue.poll(); // 任务将在延迟时间后出现在目标队列中
if (task != null) {
System.out.println("处理延迟任务:" + task);
break;
}
Thread.sleep(1000);
}
// 关闭延迟队列
delayedQueue.destroy();
-
注意事项:
RDelayedQueue
本身不能直接消费任务,需要搭配目标队列使用。- 使用完
RDelayedQueue
后,需要调用destroy()
方法释放资源。
第五部分:分布式限流
分布式限流是保护后端资源和服务稳定性的重要手段。在高并发场景下,通过限制接口请求的频率或并发数量,可以有效防止系统过载和资源耗尽。Redisson 提供了两种限流工具:信号量(RSemaphore) 和 限流器(RRateLimiter),帮助实现分布式限流。
1. 分布式限流的作用
- 限制接口请求频率:控制单位时间内允许的请求次数,防止请求过多导致系统崩溃。
- 保护后端资源:限制同时访问数据库、缓存或其他资源的并发数量,防止资源耗尽。
- 保障服务稳定性:在高并发场景下,通过限流保护服务可用性,避免出现雪崩效应。
限流算法与使用方式对比
限流算法 | Redis 限流 | 内存限流 | 网关限流 |
---|---|---|---|
固定窗口算法 | INCR 和 EXPIRE 实现简单 | 计数器本地实现 | 支持,配置较简单 |
滑动窗口算法 | 使用 ZSET 记录时间戳 | 时间戳列表实现 | 部分高级网关支持 |
漏桶算法 | 模拟流出速率,Redis 维护计数器 | 本地队列实现 | 支持,适合平滑流量控制 |
令牌桶算法 | Redis 定时补充令牌计数器 | 定时任务向桶中补充令牌 | 支持,如 Kong 或 Envoy 的限流插件 |
2. Redisson 的限流实现
Redisson 提供了两种限流工具:
2.1 信号量(RSemaphore)
特点:
- 信号量用于控制同时访问资源的线程或实例数量。
- 当信号量被消耗完时,后续的线程会进入等待状态,直到有可用的信号量。
常用方法:
方法 | 描述 |
---|---|
trySetPermits(int permits) | 初始化信号量的数量 |
acquire() | 获取一个许可,当前信号量减 1 |
acquire(int permits) | 获取指定数量的许可 |
release() | 释放一个许可,当前信号量加 1 |
release(int permits) | 释放指定数量的许可 |
使用示例:
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
// 初始化信号量,最大允许 5 个并发
semaphore.trySetPermits(5);
try {
// 获取一个许可
semaphore.acquire();
// 执行业务逻辑
System.out.println("当前线程获取到信号量,正在处理任务");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
System.out.println("当前线程释放信号量");
}
适用场景:
- 限制并发数量:例如限制同时操作数据库的线程数量。
- 共享资源保护:例如限制同时访问某个文件或缓存的线程数量。
2.2 限流器(RRateLimiter)
特点:
- 限流器基于 令牌桶算法 实现,可以限制请求的速率。
- 支持全局限流和分布式场景下的速率限制。
常用方法:
方法 | 描述 |
---|---|
trySetRate(RateType type, long rate, long interval, RateIntervalUnit unit) | 初始化限流器,设置速率和时间间隔 |
acquire() | 获取一个令牌,阻塞直到获取成功 |
acquire(int permits) | 获取指定数量的令牌,阻塞直到获取成功 |
tryAcquire() | 尝试获取一个令牌,不阻塞,返回 true 或 false |
tryAcquire(int permits) | 尝试获取指定数量的令牌,不阻塞 |
使用示例:
RRateLimiter rateLimiter = redissonClient.getRateLimiter("rateLimiter");
// 初始化限流器,每秒允许 10 次请求
rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);
try {
// 获取一个令牌
rateLimiter.acquire();
// 执行业务逻辑
System.out.println("当前线程获取到令牌,正在处理请求");
} catch (Exception e) {
e.printStackTrace();
}
适用场景:
- 接口限流:限制 API 的每秒请求次数,防止恶意请求。
- 流量控制:限制日志写入速率或外部系统的调用频率。
3. 使用示例
3.1 限制同时访问数据库的线程数量
场景描述:
- 假设一个系统需要访问数据库资源,但为了防止过多线程同时访问导致数据库连接耗尽,需要限制同时访问的线程数量为 5。
实现代码:
RSemaphore semaphore = redissonClient.getSemaphore("dbSemaphore");
semaphore.trySetPermits(5);
Runnable task = () -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取到信号量,正在访问数据库");
Thread.sleep(2000); // 模拟数据库操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 释放信号量");
}
};
// 模拟多线程访问数据库
for (int i = 0; i < 10; i++) {
new Thread(task).start();
}
3.2 限制 API 请求频率
场景描述:
- 假设某个 API 需要限制每秒最多处理 10 个请求,超过限制的请求将进入等待状态。
实现代码:
RRateLimiter rateLimiter = redissonClient.getRateLimiter("apiRateLimiter");
rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);
Runnable requestTask = () -> {
try {
rateLimiter.acquire();
System.out.println(Thread.currentThread().getName() + " 获取到令牌,处理请求");
} catch (Exception e) {
e.printStackTrace();
}
};
// 模拟多线程请求
for (int i = 0; i < 20; i++) {
new Thread(requestTask).start();
}
4. 信号量和限流器的区别
功能 | 信号量(RSemaphore) | 限流器(RRateLimiter) |
---|---|---|
限制类型 | 限制同时访问的线程数量 | 限制单位时间内的请求速率 |
实现机制 | 基于计数器,信号量耗尽后线程阻塞等待 | 基于令牌桶算法,令牌耗尽后线程阻塞等待 |
适用场景 | 并发线程数量控制 | 流量控制,接口请求速率限制 |
配置复杂度 | 简单,直接设置信号量的数量 | 较复杂,需要配置速率和时间间隔 |
5. 注意事项
-
合理设置限流参数:
- 信号量:根据共享资源的容量设置信号量的数量。
- 限流器:根据系统吞吐能力设置速率和时间间隔。
-
使用 try-finally 释放信号量:
- 在信号量的使用中,务必在
try-finally
块中释放信号量,防止资源泄露。
- 在信号量的使用中,务必在
-
避免死锁:
- 确保业务逻辑不会因为阻塞导致线程长时间持有信号量或令牌。
第六部分:Spring Boot 集成最佳实践
1. 配置中心化管理
通过 application.properties
或 application.yml
文件进行 Redis 配置的集中管理,便于维护和环境切换。
配置示例
application.properties
示例:
# Redis 主机和端口
spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis 密码(如果无密码可省略)
spring.redis.password=
# 连接池配置
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=5000
说明:
spring.redis.host
和spring.redis.port
:指定 Redis 的主机地址和端口。spring.redis.password
:用于配置 Redis 密码。- 连接池配置:
max-active
:最大连接数。max-idle
:最大空闲连接数。min-idle
:最小空闲连接数。max-wait
:获取连接的最大等待时间。
与 Redisson 的结合: Redisson 不直接使用 Spring 的 Redis 配置,但可以通过读取这些配置集中管理 Redis 的连接信息:
Redisson 配置示例:
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password:}")
private String redisPassword;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setPassword(redisPassword.isEmpty() ? null : redisPassword);
return Redisson.create(config);
}
}
2. 基于注解的分布式锁实现
为了简化分布式锁的使用,可以封装工具类,并通过自定义注解实现分布式锁的统一管理。
2.1 封装分布式锁工具类
工具类提供锁的获取和释放方法,并封装了 Redisson 的基本操作。
分布式锁工具类示例:
@Component
public class RedisLockUtil {
private final RedissonClient redissonClient;
@Autowired
public RedisLockUtil(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
// 尝试获取锁
public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
// 释放锁
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
2.2 自定义注解实现分布式锁
通过自定义注解和 AOP 实现分布式锁的统一管理。
自定义注解示例:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
String key(); // 锁的 key
long waitTime() default 5; // 等待时间(秒)
long leaseTime() default 10; // 锁持有时间(秒)
}
AOP 实现注解逻辑:
@Aspect
@Component
public class DistributedLockAspect {
private final RedisLockUtil redisLockUtil;
@Autowired
public DistributedLockAspect(RedisLockUtil redisLockUtil) {
this.redisLockUtil = redisLockUtil;
}
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
String lockKey = distributedLock.key();
long waitTime = distributedLock.waitTime();
long leaseTime = distributedLock.leaseTime();
boolean acquired = redisLockUtil.tryLock(lockKey, waitTime, leaseTime, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("Unable to acquire lock for key: " + lockKey);
}
try {
return joinPoint.proceed(); // 执行业务逻辑
} finally {
redisLockUtil.unlock(lockKey);
}
}
}
使用示例:
@Service
public class ExampleService {
@DistributedLock(key = "exampleLock", waitTime = 5, leaseTime = 10)
public void performTask() {
System.out.println("执行带锁的任务");
}
}
3. 性能优化
3.1 连接池的配置
优化 Redis 连接池可以提高高并发场景下的性能。
-
配置建议:
max-active
:根据并发请求量设置适当的最大连接数。max-idle
和min-idle
:保持一定的空闲连接,减少频繁创建连接的开销。max-wait
:确保超时的等待时间不会导致线程过多阻塞。
-
Redisson 的连接池优化:
Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setConnectionPoolSize(20) // 连接池大小 .setConnectionMinimumIdleSize(5) // 最小空闲连接数 .setConnectTimeout(10000); // 连接超时时间
3.2 序列化方式的选择
Redis 默认使用二进制数据存储,选择合适的序列化方式可以提高性能和存储效率。
-
常用序列化方式:
- JSON:可读性好,但序列化和反序列化性能稍逊。
- Kryo:高性能二进制序列化库,适合对性能要求较高的场景。
-
使用 JSON 序列化:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setKeySerializer(new StringRedisSerializer()); return template; }
-
使用 Kryo 序列化
public class KryoRedisSerializer<T> implements RedisSerializer<T> { private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(Kryo::new); @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); Output output = new Output(byteStream)) { Kryo kryo = kryoThreadLocal.get(); kryo.writeClassAndObject(output, t); output.close(); return byteStream.toByteArray(); } catch (IOException e) { throw new SerializationException("Kryo serialization error", e); } } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } try (Input input = new Input(new ByteArrayInputStream(bytes))) { Kryo kryo = kryoThreadLocal.get(); return (T) kryo.readClassAndObject(input); } catch (IOException e) { throw new SerializationException("Kryo deserialization error", e); } } }