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非阻塞分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 关于JavaEE匿名内部类和Lambda表达式的注意事项

    关于JavaEE匿名内部类和Lambda表达式的注意事项

    这篇文章主要介绍了关于JavaEE匿名内部类和Lambda表达式的注意事项,匿名内部类顾名思义是没有修饰符甚至没有名称的内部类,使用匿名内部类需要注意哪些地方,我们一起来看看吧
    2023-03-03
  • java实现时间控制的几种方案

    java实现时间控制的几种方案

    这篇文章主要介绍了java实现时间控制的几种方案,本文从多个方面给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-07-07
  • java背包问题动态规划算法分析

    java背包问题动态规划算法分析

    这篇文章主要介绍了java背包问题动态规划算法分析,想了解算法的同学一定要看一下
    2021-04-04
  • 使用Jenkins Pipeline自动化构建发布Java项目的方法

    使用Jenkins Pipeline自动化构建发布Java项目的方法

    这篇文章主要介绍了使用Jenkins Pipeline自动化构建发布Java项目的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-04-04
  • MyBatis-Plus 处理逻辑删除与查询的5种核心实现方法

    MyBatis-Plus 处理逻辑删除与查询的5种核心实现方法

    本文介绍了MyBatis-Plus在Java开发中实现逻辑删除的5种方法,包括全局配置、实体类注解、局部配置、查询过滤和多租户结合,每种方法都有其适用场景和优缺点,开发者可以根据项目需求选择最合适的方案,感兴趣的朋友跟随小编一起看看吧
    2026-03-03
  • java实现收藏功能

    java实现收藏功能

    这篇文章主要为大家详细介绍了java实现收藏功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • Java IO学习之缓冲输入流(BufferedInputStream)

    Java IO学习之缓冲输入流(BufferedInputStream)

    这篇文章主要介绍了Java IO学习之缓冲输入流(BufferedInputStream)的相关资料,需要的朋友可以参考下
    2017-02-02
  • 详解Java8中Optional的常见用法

    详解Java8中Optional的常见用法

    Opitonal是java8引入的一个新类,目的是为了解决空指针异常问题。本文将通过示例为大家详细讲讲Optional的常见用法,需要的可以参考一下
    2022-09-09
  • Java的long和bigint长度对比详解

    Java的long和bigint长度对比详解

    在本文中小编给大家分享了关于Java的long和bigint长度比较的知识点内容,有兴趣的朋友们学习参考下。
    2019-07-07
  • java中单例模式讲解

    java中单例模式讲解

    这篇文章主要介绍了java中单例模式,本文通过简单的案例,讲解了该模式在java中的使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08

最新评论