通过 Redisson防止数据重复创建

前言

通过 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 原生锁用法一致,开发 / 维护成本低。

五、关键注意事项(避坑点)

  1. 业务 Key 必须唯一:这是防重复的核心,必须确保同一单据的 businessKey 唯一(比如用户 ID + 单据类型 + 业务单号,避免不同单据的 Key 冲突);
    锁 / 标记过期时间合理:
  2. 分布式锁过期时间:略长于创建逻辑的最大耗时(比如创建逻辑最多 3 秒,锁设 5 秒);
  3. 缓存标记过期时间:足够长(比如 10 分钟),确保短时间内重复请求都能被拦截,同时避免 Redis 数据堆积;
    异常处理:锁的释放必须放在 finally 中,确保即使创建逻辑抛出异常,锁也能正常释放;
  4. Redis 高可用:生产环境建议部署 Redis 集群 / 哨兵,避免 Redis 单点故障导致锁失效。

总结

使用 Redisson 实现单据防重复创建的核心价值:

  1. 事前拦截:从后端兜底杜绝重复请求,适配分布式架构,性能远优于数据库方案;
  2. 可靠性高:自动续期、原子操作、避免死锁,解决原生 Redis 锁的痛点;
  3. 体验 & 性能双赢:用户体验更好,同时减轻数据库和业务层的无效负载;

这个方案可直接在生产环境落地,可以根据自己的业务场景选择 "分布式锁"(耗时久)或 "缓存标记"(耗时短)方案

相关推荐
海底星光2 小时前
c# 生产者消费者模式之内存/redis队列实现
redis·c#
虹科网络安全2 小时前
艾体宝新闻 | Redis 月度更新速览:2025 年 12 月
数据库·redis·缓存
七七七七076 小时前
【Redis】Ubuntu22.04安装redis++
数据库·redis·缓存
J_liaty7 小时前
Spring Security整合JWT与Redis实现权限认证
java·redis·spring·spring-security
什么都不会的Tristan7 小时前
redis-原理篇-Dict
数据库·redis·缓存
超级种码7 小时前
Redis:Redis键值淘汰策略
redis·spring·bootstrap
重生之绝世牛码7 小时前
Linux软件安装 —— Redis集群安装(三主三从)
大数据·linux·运维·数据库·redis·数据库开发·软件安装
结衣结衣.8 小时前
Redis的基本全局命令以及数据类型和内部编码
数据库·redis·bootstrap
Mao.O9 小时前
Redis三大缓存问题及布隆过滤器详解
数据库·redis·缓存