Java封装Redis非阻塞分布式锁彻底解决表单重复提交主键冲突问题

 更新时间:2026年03月23日 15:27:02   作者:墨着染霜华  
本文基于原生RedisTemplate封装通用非阻塞分布式锁工具类,实现锁逻辑与业务逻辑解耦,兼顾易用性与安全性,同时解决锁与事务配合、锁释放时机、失败重试等核心坑点,感兴趣的朋友跟随小编一起看看吧

🍓 前言

在Web表单提交、接口调用场景中,经常遇到用户重复点击、接口重复调用导致的问题:同一业务单号并发执行插入操作,事务未提交前查询无数据,最终触发数据库主键/唯一键冲突报错。

针对这类高并发重复提交问题,本文基于原生RedisTemplate封装通用非阻塞分布式锁工具类,实现锁逻辑与业务逻辑解耦,兼顾易用性与安全性,同时解决锁与事务配合、锁释放时机、失败重试等核心坑点。

📌 业务痛点回顾

  • 场景:表单提交接口,根据业务单号执行插入操作
  • 问题:第一次请求未执行完、事务未提交,第二次请求进来查询数据库无数据,也执行插入,导致主键冲突
  • 需求:非阻塞获取锁(获取失败直接提示操作中)、执行成功禁止重复提交、执行失败释放锁允许重试、60秒兜底防死锁

🛠️ 前提准备

基于已有的RedisService工具类(包含setIfAbsent、Lua脚本释放锁),SpringBoot环境,JDK8+(函数式接口封装)

1. 原有RedisService核心锁方法(精简版)

@Slf4j
@Component
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public class RedisService {
    @Autowired
    public RedisTemplate redisTemplate;
    /**
     * 非阻塞获取分布式锁(原子操作)
     * @param lockKey 锁键
     * @param requestId 唯一标识,防止误删锁
     * @param expireTime 过期时间(秒)
     * @return 获取结果
     */
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        try {
            Boolean result = redisTemplate.opsForValue().setIfAbsent(
                    lockKey,
                    requestId,
                    expireTime,
                    TimeUnit.SECONDS
            );
            return Boolean.TRUE.equals(result);
        } catch (Exception e) {
            log.error("获取分布式锁失败, lockKey:{}", lockKey, e);
            return false;
        }
    }
    /**
     * 安全释放锁(Lua脚本保证原子性)
     * @param lockKey 锁键
     * @param requestId 唯一标识
     * @return 释放结果
     */
    public boolean releaseLock(String lockKey, String requestId) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        try {
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(luaScript);
            redisScript.setResultType(Long.class);
            Long result = (Long) redisTemplate.execute(
                    redisScript,
                    Collections.singletonList(lockKey),
                    requestId
            );
            return result != null && result > 0;
        } catch (Exception e) {
            log.error("释放分布式锁失败, lockKey:{}", lockKey, e);
            return false;
        }
    }
    // 省略其他缓存方法...
}

🚀 核心封装:通用分布式锁工具类

利用Supplier函数式接口,将锁的获取、释放、异常处理封装成公共方法,业务代码无需重复编写锁逻辑,只需关注核心业务。

RedisLockUtil 封装类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.function.Supplier;
/**
 * 通用Redis非阻塞分布式锁工具类
 * 适配:表单防重、接口防重复提交、并发插入控制
 */
@Slf4j
@Component
public class RedisLockUtil {
    @Autowired
    private RedisService redisService;
    /**
     * 非阻塞锁执行通用方法
     * @param lockKey 锁唯一键(建议:lock:业务模块:业务单号)
     * @param expireSeconds 锁过期时间(秒,兜底防死锁)
     * @param businessLogic 业务逻辑代码块
     * @param <T> 返回值泛型
     * @return 业务执行结果
     */
    public <T> T executeWithLock(String lockKey, int expireSeconds, Supplier<T> businessLogic) {
        // 生成唯一请求ID,防止误删其他线程的锁
        String requestId = UUID.randomUUID().toString();
        // 1. 非阻塞获取锁
        boolean isLocked = redisService.tryLock(lockKey, requestId, expireSeconds);
        if (!isLocked) {
            log.warn("[分布式锁] 获取失败,锁键:{},操作进行中", lockKey);
            throw new RuntimeException("操作进行中,请勿重复提交!");
        }
        try {
            // 2. 执行核心业务逻辑
            return businessLogic.get();
        } catch (Exception e) {
            log.error("[分布式锁] 业务执行失败,锁键:{},允许重试", lockKey, e);
            throw new RuntimeException("提交失败:" + e.getMessage() + ",可重试!");
        } finally {
            // 3. 无论成功/失败,必须释放锁
            boolean isReleased = redisService.releaseLock(lockKey, requestId);
            if (!isReleased) {
                log.warn("[分布式锁] 释放失败,锁键:{},requestId:{}(可能已过期自动释放)", lockKey, requestId);
            }
        }
    }
}

🎯 实战调用:表单提交防重

调用封装好的工具类,代码极度简洁,锁逻辑完全剥离,业务代码更清晰。

业务层调用代码

@Service
@Slf4j
public class FormSubmitService {
    @Autowired
    private RedisLockUtil redisLockUtil;
    // 业务Mapper,隐藏具体表信息
    @Autowired
    private BizFormMapper bizFormMapper;
    /**
     * 表单提交核心方法
     * @param djbh 业务单号(唯一标识)
     * @param formDTO 表单参数
     * @return 接口返回结果
     */
    public Result submitForm(String djbh, BizFormDTO formDTO) {
        try {
            // 构造锁键:隐藏具体业务模块,规范命名
            String lockKey = "lock:biz_form_submit:" + djbh;
            // 调用锁工具类,60秒过期兜底,执行业务逻辑
            return redisLockUtil.executeWithLock(lockKey, 60, () -> {
                // ========== 核心业务逻辑 ==========
                // 1. 查询是否已存在(成功后禁止重复提交)
                Object existData = bizFormMapper.selectByDjbh(djbh);
                if (existData != null) {
                    return Result.fail("该单据已提交,请勿重复操作!");
                }
                // 2. 执行插入操作
                bizFormMapper.insertForm(formDTO);
                return Result.success("提交成功!");
                // ================================
            });
        } catch (RuntimeException e) {
            // 捕获锁异常、业务异常,统一返回
            return Result.fail(e.getMessage());
        }
    }
    // 省略Result成功/失败封装方法...
}

⚠️ 关键避坑点(必看)

1. 锁与事务的配合问题

错误做法:锁写在@Transactional事务内部,导致锁释放早于事务提交,仍有并发冲突风险。

正确做法:锁包裹事务,本文封装方式天然满足(锁在事务外层,事务提交后再释放锁)。

2. 锁释放时机

  • 执行成功:finally块主动释放锁,后续请求进来会查询数据库已存在,直接拒绝,无风险
  • 执行失败:释放锁,允许用户重试提交
  • 服务宕机:Redis 60秒自动过期,避免死锁

3. 锁键设计规范

遵循 lock:业务模块:唯一单号 格式,保证锁的细粒度,避免全局锁影响性能。

4. 非阻塞特性

获取锁失败直接返回提示,不阻塞线程,提升用户体验和接口性能,适配表单提交场景。

📊 方案优势

  • 解耦彻底:锁逻辑与业务代码分离,一处封装,多处复用
  • 安全可靠:requestId防误删、Lua脚本释放、过期兜底,杜绝死锁
  • 易用简洁:调用时只需传入锁键、过期时间、业务代码块,零冗余
  • 适配场景:表单防重、接口限流、并发插入控制、幂等性保证

📝 总结

针对Java后端表单重复提交、并发主键冲突问题,通过Redis非阻塞分布式锁+函数式封装,既能从根源解决并发问题,又能保证代码优雅性。核心思路是锁控制并发,数据库状态控制幂等,双层保障杜绝重复提交。

本文封装的工具类可直接接入项目,适配绝大多数防重复提交场景,无需重复造轮子。

到此这篇关于Java封装Redis非阻塞分布式锁彻底解决表单重复提交主键冲突问题的文章就介绍到这了,更多相关java redis非阻塞分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IntelliJ IDEA中Scala、sbt、maven配置教程

    IntelliJ IDEA中Scala、sbt、maven配置教程

    这篇文章主要介绍了IntelliJ IDEA中Scala、sbt、maven配置教程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Java NoClassDefFoundError运行时错误分析解决

    Java NoClassDefFoundError运行时错误分析解决

    在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能找到该类的定义,这个问题经常与类路径配置不正确或者缺失的库文件有关,需要的朋友可以参考下
    2025-05-05
  • jstack+jdb命令查看线程及死锁堆栈信息的实例

    jstack+jdb命令查看线程及死锁堆栈信息的实例

    这篇文章主要介绍了jstack+jdb命令查看线程及死锁堆栈信息的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • 解读@RequestBody与post请求的关系

    解读@RequestBody与post请求的关系

    这篇文章主要介绍了解读@RequestBody与post请求的关系,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • Java实现轻松提取word和pdf文档内容

    Java实现轻松提取word和pdf文档内容

    这篇文章主要为大家详细介绍了如何使用Java实现轻松提取word和pdf文档内容,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-10-10
  • SpringBoot3整合Mybatis完整版实例

    SpringBoot3整合Mybatis完整版实例

    本文详细介绍了SpringBoot3整合MyBatis的完整步骤,包括添加数据库驱动和MyBatis依赖、配置数据源和MyBatis、创建表和Bean类、编写Mapper接口和XML文件、创建Controller类以及配置扫描包,通过这些步骤,可以实现SpringBoot3与MyBatis的成功整合,并进行功能测试
    2025-01-01
  • Java五子棋简单实现代码举例

    Java五子棋简单实现代码举例

    Java五子棋游戏是一种经典的两人对战棋类游戏,它基于简单的规则,即任何一方的棋子在棋盘上形成连续的五个,无论是横、竖还是斜线,都将获胜,这篇文章主要介绍了Java五子棋实现的相关资料,需要的朋友可以参考下
    2024-10-10
  • SpringBoot Actuator未授权访问漏洞的排查和解决方法

    SpringBoot Actuator未授权访问漏洞的排查和解决方法

    Spring Boot Actuator 是开发和管理生产级 Spring Boot 应用程序的重要工具,它可以帮助你确保应用程序的稳定性和性能,本文给大家介绍了SpringBoot Actuator未授权访问漏洞的排查和解决方法,需要的朋友可以参考下
    2024-05-05
  • 使用ElasticSearch6.0快速实现全文搜索功能的示例代码

    使用ElasticSearch6.0快速实现全文搜索功能的示例代码

    本篇文章主要介绍了使用ElasticSearch6.0快速实现全文搜索功能,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • 实例讲解MyBatis如何防止SQL注入

    实例讲解MyBatis如何防止SQL注入

    这篇文章通过实例代码介绍MyBatis如何防止SQL注入,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-12-12

最新评论