通过 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. 体验 & 性能双赢:用户体验更好,同时减轻数据库和业务层的无效负载;

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

相关推荐
二宝哥11 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
ShiXZ21317 小时前
Redis docker-compose 部署指南
redis·docker·eureka
青山木20 小时前
Redis 缓存一致性全解:从 Cache Aside 到延迟双删、Canal+MQ 的进阶之路
java·数据库·redis·后端·缓存
YDS82920 小时前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
爬也要爬着前进1 天前
redis主从搭建
数据库·redis·缓存
用户095367515832 天前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌2 天前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆2 天前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰2 天前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_727535622 天前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf