Spring Boot 整合 Redisson 实现分布式锁:实战指南
在分布式系统中,分布式锁是解决并发问题的关键组件。Redisson 作为 Redis 官方推荐的 Java 客户端,提供了强大的分布式锁实现,支持自动续期、可重入性、公平锁等高级特性。本文将专注于如何使用 Redisson 框架在 Spring Boot 项目中实现分布式锁,从环境搭建到实战应用,一步到位。
一、Redisson 简介与优势
Redisson 是一个基于 Redis 的 Java 分布式框架,不仅提供了 Redis 客户端功能,更封装了一系列分布式工具类,其中分布式锁是其最常用的功能之一。
Redisson 分布式锁的核心优势:
- 自动续期:内置"看门狗"机制,当业务未执行完时自动延长锁的过期时间,避免锁提前释放
- 可重入性:支持同一线程多次获取锁,避免死锁
- 集群支持:提供红锁(RedLock)机制,解决 Redis 集群下的"脑裂"问题
- 丰富的锁类型:除了普通锁,还支持公平锁、读写锁、联锁等
- 高性能:基于 Netty 实现,非阻塞 IO,性能优于传统 Jedis 客户端
二、环境准备
2.1 版本兼容说明
Redisson 版本需要与 Redis 版本兼容,推荐组合:
- Redis 6.x + Redisson 3.16.x 及以上
- Redis 5.x + Redisson 3.14.x 及以上
本文使用:Spring Boot 2.7.10 + Redisson 3.23.3 + Redis 6.2.6
2.2 引入依赖
在 pom.xml
中添加 Redisson 依赖(Spring Boot 专用 starter):
xml
<!-- Redisson Spring Boot Starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
<!-- Spring Boot Web(用于测试接口) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2.3 配置 Redisson
Redisson 支持多种配置方式(YAML/JSON/Java 代码),推荐使用 YAML 配置,在 application.yml
中添加:
yaml
spring:
application:
name: redisson-lock-demo
# Redisson 配置
redisson:
# 超时配置
connect-timeout: 3000 # 连接超时时间(毫秒)
timeout: 3000 # 命令等待超时时间(毫秒)
# 单节点配置(生产环境常用,简单部署)
single-server-config:
address: redis://localhost:6379 # Redis 地址
password: 123456 # Redis 密码(无密码可省略)
database: 0 # 数据库索引
connection-pool-size: 16 # 连接池大小
idle-connection-timeout: 30000 # 空闲连接超时时间(毫秒)
ping-interval: 0 # 心跳检测间隔(0表示不检测)
# 集群配置(多节点高可用,按需开启)
# cluster-servers-config:
# node-addresses:
# - redis://192.168.1.101:6379
# - redis://192.168.1.102:6379
# - redis://192.168.1.103:6379
# scan-interval: 2000 # 集群节点扫描间隔(毫秒)
配置说明:
- 单节点配置适用于开发环境或简单生产环境
- 集群配置适用于高可用场景,需指定所有节点地址
connection-pool-size
建议根据并发量调整(默认 64,过小可能导致连接不足)
三、Redisson 分布式锁核心用法
Redisson 提供了 RLock
接口作为分布式锁的核心 API,支持多种锁操作。以下是最常用的锁类型及用法:
3.1 可重入锁(Reentrant Lock)
最常用的锁类型,支持同一线程多次获取锁,避免自己锁死自己。
java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class RedissonLockService {
// 注入 Redisson 客户端
@Resource
private RedissonClient redissonClient;
// 锁的 key(按业务场景定义,如商品ID、订单ID)
private static final String LOCK_KEY = "product:lock:%s"; // %s 用于替换业务ID
/**
* 获取锁(阻塞式)
* @param businessId 业务ID(如商品ID)
* @return 锁对象
*/
public RLock getLock(String businessId) {
String lockKey = String.format(LOCK_KEY, businessId);
return redissonClient.getLock(lockKey);
}
/**
* 加锁并执行业务(自动续期)
* @param businessId 业务ID
* @param task 业务任务
*/
public void executeWithLock(String businessId, Runnable task) {
RLock lock = getLock(businessId);
try {
// 加锁(30秒过期,Redisson 会自动续期)
lock.lock(30, TimeUnit.SECONDS);
// 执行业务
task.run();
} finally {
// 释放锁(仅当前线程持有锁时才释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 尝试加锁(非阻塞式)
* @param businessId 业务ID
* @param waitTime 最多等待时间
* @param leaseTime 锁持有时间
* @param task 业务任务
* @return 是否加锁成功并执行
*/
public boolean tryExecuteWithLock(String businessId, long waitTime, long leaseTime,
TimeUnit unit, Runnable task) {
RLock lock = getLock(businessId);
try {
// 尝试加锁:最多等 waitTime,持有 leaseTime 后自动释放
boolean locked = lock.tryLock(waitTime, leaseTime, unit);
if (locked) {
task.run();
return true;
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
3.2 公平锁(Fair Lock)
公平锁保证线程获取锁的顺序与请求顺序一致,避免"饥饿"现象(某些线程长期获取不到锁)。适用于对顺序性要求高的场景。
java
/**
* 获取公平锁
*/
public RLock getFairLock(String businessId) {
String lockKey = String.format(LOCK_KEY, businessId);
return redissonClient.getFairLock(lockKey);
}
// 使用方式与可重入锁一致
public void executeWithFairLock(String businessId, Runnable task) {
RLock fairLock = getFairLock(businessId);
try {
fairLock.lock(30, TimeUnit.SECONDS);
task.run();
} finally {
if (fairLock.isHeldByCurrentThread()) {
fairLock.unlock();
}
}
}
3.3 读写锁(ReadWrite Lock)
适用于"多读少写"场景,允许多个读操作并发执行,但写操作需要独占锁,提高读操作的并发效率。
java
import org.redisson.api.RReadWriteLock;
/**
* 读写锁示例:查询用读锁,更新用写锁
*/
public class ReadWriteLockService {
@Resource
private RedissonClient redissonClient;
private static final String RW_LOCK_KEY = "product:rwlock:%s";
/**
* 读操作(共享锁,可并发)
*/
public void readData(String businessId, Runnable readTask) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock(String.format(RW_LOCK_KEY, businessId));
RLock readLock = rwLock.readLock();
try {
readLock.lock(30, TimeUnit.SECONDS);
readTask.run();
} finally {
if (readLock.isHeldByCurrentThread()) {
readLock.unlock();
}
}
}
/**
* 写操作(独占锁,互斥)
*/
public void writeData(String businessId, Runnable writeTask) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock(String.format(RW_LOCK_KEY, businessId));
RLock writeLock = rwLock.writeLock();
try {
writeLock.lock(30, TimeUnit.SECONDS);
writeTask.run();
} finally {
if (writeLock.isHeldByCurrentThread()) {
writeLock.unlock();
}
}
}
}
四、实战场景:分布式库存扣减
以电商库存扣减为例,演示 Redisson 锁如何解决超卖问题。
4.1 库存服务实现
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class StockService {
@Resource
private StringRedisTemplate stringRedisTemplate; // Spring 提供的 Redis 操作工具
@Resource
private RedissonLockService redissonLockService; // 上文实现的锁服务
// 库存 Redis Key(格式:stock:{商品ID})
private static final String STOCK_KEY = "stock:%s";
/**
* 初始化库存
*/
public void initStock(String productId, int initialStock) {
stringRedisTemplate.opsForValue().set(String.format(STOCK_KEY, productId), String.valueOf(initialStock));
log.info("初始化库存成功,商品ID:{},初始库存:{}", productId, initialStock);
}
/**
* 扣减库存(无锁版本,高并发下会超卖)
*/
public boolean deductStockWithoutLock(String productId) {
String stockKey = String.format(STOCK_KEY, productId);
Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(stockKey));
if (stock == null || stock <= 0) {
log.error("库存不足,商品ID:{}", productId);
return false;
}
// 高并发下此处会超卖(非原子操作)
stringRedisTemplate.opsForValue().set(stockKey, String.valueOf(stock - 1));
log.info("扣减库存成功,商品ID:{},剩余库存:{}", productId, stock - 1);
return true;
}
/**
* 扣减库存(Redisson 锁版本,避免超卖)
*/
public boolean deductStockWithLock(String productId) {
// 使用 Redisson 锁执行扣减逻辑
final boolean[] result = {false};
redissonLockService.executeWithLock(productId, () -> {
String stockKey = String.format(STOCK_KEY, productId);
Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(stockKey));
if (stock != null && stock > 0) {
stringRedisTemplate.opsForValue().set(stockKey, String.valueOf(stock - 1));
log.info("扣减库存成功,商品ID:{},剩余库存:{}", productId, stock - 1);
result[0] = true;
} else {
log.error("库存不足,商品ID:{}", productId);
result[0] = false;
}
});
return result[0];
}
/**
* 查询库存
*/
public Integer getStock(String productId) {
String stock = stringRedisTemplate.opsForValue().get(String.format(STOCK_KEY, productId));
return stock == null ? 0 : Integer.parseInt(stock);
}
}
4.2 接口暴露(用于测试)
java
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/stock")
public class StockController {
@Resource
private StockService stockService;
/**
* 初始化库存
*/
@PostMapping("/init")
public String initStock(@RequestParam String productId, @RequestParam int initialStock) {
stockService.initStock(productId, initialStock);
return "初始化成功,商品ID:" + productId + ",库存:" + initialStock;
}
/**
* 无锁扣减库存(测试超卖用)
*/
@PostMapping("/deduct/no-lock")
public String deductWithoutLock(@RequestParam String productId) {
boolean success = stockService.deductStockWithoutLock(productId);
return success ? "扣减成功" : "库存不足";
}
/**
* Redisson 锁扣减库存
*/
@PostMapping("/deduct/redisson-lock")
public String deductWithLock(@RequestParam String productId) {
boolean success = stockService.deductStockWithLock(productId);
return success ? "扣减成功" : "库存不足";
}
/**
* 查询库存
*/
@GetMapping("/query")
public String queryStock(@RequestParam String productId) {
return "商品ID:" + productId + ",当前库存:" + stockService.getStock(productId);
}
}
4.3 高并发测试(JMeter)
-
测试步骤:
- 初始化库存:
POST http://localhost:8080/stock/init?productId=1001&initialStock=100
- 用 JMeter 创建 1000 个线程并发请求扣减接口
- 分别测试无锁版本和 Redisson 锁版本,观察最终库存
- 初始化库存:
-
测试结果:
- 无锁版本:最终库存可能为负数(如 -23),出现超卖
- Redisson 锁版本:最终库存为 0,无超卖,并发安全
五、Redisson 锁高级特性解析
5.1 自动续期(看门狗机制)
当调用 lock(30, TimeUnit.SECONDS)
时,Redisson 会启动一个后台线程(看门狗),每隔 10 秒(30/3)检查锁是否仍被当前线程持有:
- 若持有,则延长锁的过期时间至 30 秒
- 若业务执行完成(调用
unlock()
),则停止续期 - 若线程意外终止,看门狗也会终止,锁会在 30 秒后自动释放
优势:无需预估业务执行时间,避免锁提前释放导致的并发问题。
5.2 红锁(RedLock)机制
针对 Redis 主从集群的"脑裂"问题(主节点宕机,从节点未同步锁信息),Redisson 实现了红锁算法:
- 同时向多个独立的 Redis 节点(通常 5 个)请求加锁
- 只有超过半数(≥3)节点加锁成功,才认为总锁加锁成功
- 释放锁时,向所有节点发送释放请求
使用方式:
java
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisClient;
import java.util.Arrays;
import java.util.List;
public class RedLockService {
@Resource
private RedissonClient redissonClient;
public void executeWithRedLock(String businessId, Runnable task) {
// 多个独立 Redis 节点的锁
List<String> nodeAddresses = Arrays.asList(
"redis://192.168.1.101:6379",
"redis://192.168.1.102:6379",
"redis://192.168.1.103:6379",
"redis://192.168.1.104:6379",
"redis://192.168.1.105:6379"
);
// 创建红锁
RLock[] locks = nodeAddresses.stream()
.map(address -> {
// 连接每个节点并获取锁
RedisClient client = RedisClient.create(address);
return client.getLock("product:lock:" + businessId);
})
.toArray(RLock[]::new);
RLock redLock = redissonClient.getRedLock(locks);
try {
// 红锁加锁(等待100ms,持有30秒)
boolean locked = redLock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
if (locked) {
task.run();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (redLock.isHeldByCurrentThread()) {
redLock.unlock();
}
}
}
}
六、最佳实践与注意事项
-
锁的粒度要细:
- 错误示例:用一个全局锁
stock:lock
控制所有商品的库存 - 正确示例:按商品ID加锁
stock:lock:1001
,不同商品的锁互不影响,提高并发效率
- 错误示例:用一个全局锁
-
避免长时间持有锁:
- 锁内逻辑应尽量简短,避免 IO 密集型操作(如数据库查询、远程调用)
- 若必须执行耗时操作,可考虑异步处理,缩短锁持有时间
-
释放锁的正确姿势:
- 必须在
finally
中释放锁,确保业务异常时也能释放 - 释放前判断
lock.isHeldByCurrentThread()
,避免释放其他线程的锁
- 必须在
-
集群环境推荐红锁:
- 单节点 Redis 存在单点故障风险,生产环境建议用 Redis 集群 + 红锁机制
-
监控与告警:
- 监控锁的获取成功率、等待时间、续期次数
- 当锁等待时间过长时触发告警,可能预示并发过高或业务逻辑耗时过长
七、总结
Redisson 凭借强大的功能和易用性,成为 Spring Boot 项目实现分布式锁的首选框架。本文从环境搭建、核心用法到实战场景,详细介绍了 Redisson 分布式锁的使用,重点包括:
- 可重入锁解决基本并发问题
- 公平锁保证请求顺序
- 读写锁优化"多读少写"场景
- 红锁机制保证集群环境下的可靠性
掌握 Redisson 分布式锁,能有效解决分布式系统中的超卖、重复提交、资源竞争等问题,为高并发业务保驾护航。
如果本文对你有帮助,欢迎点赞收藏,如有疑问或补充,欢迎在评论区交流!