前言
通过 Redisson 实现单据防重复创建的需求,核心是解决高并发场景下(比如用户重复点击提交按钮、接口被重复调用)同一单据被多次创建的问题。
一、核心问题分析
单据重复创建的本质是:同一业务标识(比如用户 ID + 单据类型 + 业务单号)在短时间内被多次触发创建逻辑,且传统的数据库唯一索引、乐观锁等方案存在各自的局限性(比如索引仅能事后报错,乐观锁需要重试)。
Redisson 基于 Redis 提供的分布式锁 / 分布式对象能力,能在事前拦截重复请求,是高并发场景下防重复的最优方案之一。
二、可执行的完整方案(Java + Redisson)
步骤 1:环境准备
首先引入 Redisson 依赖(以 Maven 为例):
xml
<!-- Redisson 核心依赖(适配 Redis 6.x/7.x) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.3</version>
</dependency>
<!-- Spring Boot 整合 Redisson 可选(简化配置) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
步骤 2:Redisson 配置(Spring Boot 示例)
在 application.yml 中配置 Redis 连接:
yml
spring:
redis:
host: 127.0.0.1
port: 6379
password: # 无密码则留空
database: 0
redisson:
config: |
singleServerConfig:
address: "redis://${spring.redis.host}:${spring.redis.port}"
password: ${spring.redis.password}
database: ${spring.redis.database}
# 连接池配置(按需调整)
connectionPoolSize: 10
connectionMinimumIdleSize: 5
# 锁超时配置(核心:避免死锁)
lockWatchdogTimeout: 30000 # 看门狗自动续期时间(30秒)
步骤 3:核心实现逻辑(两种方案,按需选择)
单据防重复创建有两种核心场景,对应两种 Redisson 方案:
方案 1:分布式锁(适合 "创建逻辑耗时较长" 的场景)
核心思路:为每个唯一的单据业务标识生成一把分布式锁,只有获取到锁的请求才能执行创建逻辑,其他请求直接拒绝。
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 BillCreateService {
@Resource
private RedissonClient redissonClient;
// 锁前缀(便于区分业务)
private static final String BILL_LOCK_PREFIX = "bill:create:lock:";
// 锁过期时间(根据创建逻辑耗时调整,建议3-10秒)
private static final long LOCK_EXPIRE_TIME = 5;
private static final TimeUnit LOCK_TIME_UNIT = TimeUnit.SECONDS;
/**
* 创建单据(防重复)
* @param businessKey 唯一业务标识(比如:用户ID+单据类型+业务单号,如 "user123:order:20260119001")
* @param billData 单据数据
* @return 创建结果
*/
public String createBill(String businessKey, Object billData) {
// 1. 生成唯一锁Key
String lockKey = BILL_LOCK_PREFIX + businessKey;
RLock lock = redissonClient.getLock(lockKey);
try {
// 2. 尝试获取锁(非阻塞,立即返回结果)
// tryLock(等待时间, 过期时间, 时间单位):0表示不等待,直接判断是否能获取锁
boolean isLockSuccess = lock.tryLock(0, LOCK_EXPIRE_TIME, LOCK_TIME_UNIT);
if (!isLockSuccess) {
// 获取锁失败 = 有其他请求正在创建,直接返回重复创建提示
return "请勿重复提交,单据正在创建中!";
}
// 3. 获取锁成功:执行真实的创建逻辑(数据库插入、业务处理等)
// ========== 以下是你的业务逻辑 ==========
boolean isCreateSuccess = doCreateBill(billData);
if (isCreateSuccess) {
return "单据创建成功!";
} else {
return "单据创建失败,请重试!";
}
// ========== 业务逻辑结束 ==========
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "单据创建异常,请重试!";
} finally {
// 4. 释放锁(必须在finally中,避免死锁)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 真实的单据创建逻辑(你的业务代码)
*/
private boolean doCreateBill(Object billData) {
// 示例:插入数据库、调用其他接口等
System.out.println("执行单据创建逻辑:" + billData);
return true;
}
}
方案 2:分布式信号量 / 缓存标记(适合 "创建逻辑耗时短" 的场景)
核心思路:创建成功后,在 Redis 中设置一个带过期时间的标记,后续请求先检查标记是否存在,存在则拒绝创建(比分布式锁更轻量)。
java
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class BillCreateService {
@Resource
private RedissonClient redissonClient;
// 标记前缀
private static final String BILL_MARK_PREFIX = "bill:create:mark:";
// 标记过期时间(防止Redis数据堆积,建议5-10分钟)
private static final long MARK_EXPIRE_TIME = 10;
private static final TimeUnit MARK_TIME_UNIT = TimeUnit.MINUTES;
public String createBill(String businessKey, Object billData) {
// 1. 生成唯一标记Key
String markKey = BILL_MARK_PREFIX + businessKey;
RBucket<String> bucket = redissonClient.getBucket(markKey);
// 2. 检查标记是否存在(原子操作,避免并发问题)
boolean isMarkExist = bucket.isExists();
if (isMarkExist) {
return "该单据已创建,请勿重复提交!";
}
// 3. 执行创建逻辑
boolean isCreateSuccess = doCreateBill(billData);
if (isCreateSuccess) {
// 4. 创建成功后设置标记(原子操作,带过期时间)
bucket.set("CREATED", MARK_EXPIRE_TIME, MARK_TIME_UNIT);
return "单据创建成功!";
} else {
return "单据创建失败,请重试!";
}
}
private boolean doCreateBill(Object billData) {
// 业务逻辑
return true;
}
}
步骤 4:调用示例(Controller 层)
java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/bill")
public class BillController {
@Resource
private BillCreateService billCreateService;
@PostMapping("/create")
public String createBill(
@RequestParam("userId") String userId,
@RequestParam("billType") String billType,
@RequestParam("businessNo") String businessNo,
@RequestParam("billData") String billData) {
// 生成唯一业务Key(核心:确保同一单据的业务Key唯一)
String businessKey = userId + ":" + billType + ":" + businessNo;
return billCreateService.createBill(businessKey, billData);
}
}
三、为什么选择 Redisson 做这件事?(方案设计原因)
1. 对比传统方案,Redisson 的核心优势:


2. Redisson 本身的特性适配场景
分布式锁的可靠性:Redisson 的 RLock 实现了 Java 的 Lock 接口,且基于 Redis 的 Redlock 算法(可选),解决了原生 Redis 锁的单点故障问题;
自动续期(看门狗):如果创建逻辑耗时超过锁过期时间,Redisson 会自动给锁续期,避免 "锁提前释放导致重复创建";
原子操作:Redisson 的 RBucket(缓存标记)、RLock 的所有操作都是原子的,避免并发场景下的 "检查 - 设置" 竞态问题;
轻量且高性能:Redis 是内存数据库,Redisson 操作的响应时间在毫秒级,远快于数据库操作,适合高并发拦截。
四、使用 Redisson 带来的核心好处
1. 技术层面
100% 拦截重复请求:事前拦截,而非事后报错,用户体验更好;
适配分布式架构:支持多服务节点、多机房部署,解决本地锁失效问题;
避免死锁风险:Redisson 自动续期、自动释放锁(即使服务宕机,锁过期后也会自动释放),无死锁隐患;
高性能:Redis 内存操作,TPS 可达 10W+,支撑高并发场景(比如秒杀、大促的单据创建);
易于扩展:Redisson 还支持限流器(RateLimiter)、信号量(Semaphore)等,可按需扩展防重复逻辑。
2. 业务层面
数据一致性:杜绝重复单据,避免财务 / 业务数据混乱;
系统稳定性:减少无效的数据库插入、业务逻辑执行,降低系统负载;
用户体验:用户重复点击时,直接返回友好提示,而非创建失败 / 数据错误;
可维护性:API 简洁,与 Java 原生锁用法一致,开发 / 维护成本低。
五、关键注意事项(避坑点)
- 业务 Key 必须唯一:这是防重复的核心,必须确保同一单据的 businessKey 唯一(比如用户 ID + 单据类型 + 业务单号,避免不同单据的 Key 冲突);
锁 / 标记过期时间合理: - 分布式锁过期时间:略长于创建逻辑的最大耗时(比如创建逻辑最多 3 秒,锁设 5 秒);
- 缓存标记过期时间:足够长(比如 10 分钟),确保短时间内重复请求都能被拦截,同时避免 Redis 数据堆积;
异常处理:锁的释放必须放在 finally 中,确保即使创建逻辑抛出异常,锁也能正常释放; - Redis 高可用:生产环境建议部署 Redis 集群 / 哨兵,避免 Redis 单点故障导致锁失效。
总结
使用 Redisson 实现单据防重复创建的核心价值:
- 事前拦截:从后端兜底杜绝重复请求,适配分布式架构,性能远优于数据库方案;
- 可靠性高:自动续期、原子操作、避免死锁,解决原生 Redis 锁的痛点;
- 体验 & 性能双赢:用户体验更好,同时减轻数据库和业务层的无效负载;
这个方案可直接在生产环境落地,可以根据自己的业务场景选择 "分布式锁"(耗时久)或 "缓存标记"(耗时短)方案