高并发导致重复key问题--org.springframework.dao.DuplicateKeyException

文章目录

  • [1. 前言](#1. 前言)
  • [2. 问题描述](#2. 问题描述)
  • [3. 问题分析](#3. 问题分析)
  • [4. 解决方案](#4. 解决方案)
    • 数据库层
    • 应用层
      • [代码改造示例 1](#代码改造示例 1)
      • [代码改造示例 2 -- 我自己的解决方案](#代码改造示例 2 -- 我自己的解决方案)
  • [5. 结束](#5. 结束)

1. 前言

高并发场景下,数据库操作可能因线程竞争导致重复唯一键(DuplicateKey)异常。此类问题常出现在分布式系统或高流量业务中,需从代码、数据库、并发控制等多维度分析解决。

2. 问题描述

org.springframework.dao.DuplicateKeyException 是 Spring 对数据库唯一键冲突的封装异常,通常由以下操作触发:

  • 插入或更新数据时违反唯一约束(如主键、唯一索引)。
  • 并发请求同时插入相同唯一键值,数据库仅允许其中一个成功。

典型场景:用户注册时用户名唯一性检查与插入操作非原子性,导致多个线程同时通过检查后重复插入。

3. 问题分析

根本原因

  1. 非原子性操作:先查询后插入的分步操作,缺乏事务或锁保护。
  2. 数据库隔离级别不足 :如使用 READ_COMMITTED 隔离级别,无法防止幻读。
  3. 唯一索引设计缺陷:未覆盖业务场景的全部约束条件。

并发竞争逻辑

复制代码
Thread A → 检查key不存在 → 准备插入  
Thread B → 检查key不存在 → 插入成功  
Thread A → 插入失败(DuplicateKeyException)  

问题重现

模拟代码 1

java 复制代码
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public String register(String username) {
        // 高并发时多个线程可能同时通过检查
        if (!userService.existsByUsername(username)) {
            userService.save(new User(username));
            return "Success";
        }
        return "Username exists";
    }
}

模拟代码 2---我自己遇到的问题

java 复制代码
@Override
	public R isExistByFile(String fileName) {
		if (minioTemplate.isExist(fileProperties.getBucketName(),fileName)){
			//文件路径----字词卡片子卡片需要有唯一id标识
			if (fileName.contains("生成字词卡片音频")){
				HashMap<Object, Object> result;
				TeachExerciseFile exerciseFile = new TeachExerciseFile();
				exerciseFile.setPath(fileName);
				//文件名称
				exerciseFile.setName(fileName.substring(fileName.lastIndexOf("/"), fileName.lastIndexOf(".")));
				//文件后缀
				String extend = fileName.substring(fileName.lastIndexOf(".") + 1);
				exerciseFile.setExt(extend);
				teachExerciseFileMapper.insert(exerciseFile);
				result = new HashMap<>();
				result.put("file_id", exerciseFile.getId());
				result.put("file_url", exerciseFile.getPath());
				return R.ok().setCode(CommonConstants.SUCCESS).setData(result);
			}
			return R.ok().setCode(CommonConstants.SUCCESS).setData(fileName);
		}
		return R.ok().setCode(CommonConstants.FAIL);
	}

触发条件

  • 使用 JMeter 或 Postman 模拟 100+ 并发注册同一用户名。
  • 数据库日志显示多条 INSERT 语句尝试,最终抛出 DuplicateKeyException

4. 解决方案

数据库层

  • 唯一索引强制约束 :确保表字段有唯一索引,如 ALTER TABLE users ADD UNIQUE (username)
  • ON DUPLICATE KEY UPDATE:MySQL 特有语法,冲突时转为更新。

应用层

  • 悲观锁 :在查询前加锁(如 SELECT FOR UPDATE),但影响性能。
  • 乐观锁:通过版本号控制,但需重试机制。
  • 分布式锁:Redis 或 Zookeeper 实现互斥锁,确保唯一性检查与插入原子性。

代码改造示例 1

java 复制代码
@Transactional
public String registerWithLock(String username) {
    // 先尝试插入,依赖数据库唯一索引
    try {
        userRepository.save(new User(username));
        return "Success";
    } catch (DuplicateKeyException e) {
        return "Username exists";
    }
}

代码改造示例 2 -- 我自己的解决方案

java 复制代码
// -----------------------------------修改点---定义一个 ReentrantLock 对象
private static final ReentrantLock insertLock = new ReentrantLock();
@Override
public R isExistByFile(String fileName) {
	if (minioTemplate.isExist(fileProperties.getBucketName(),fileName)){
		//文件路径----字词卡片子卡片需要有唯一id标识
		if (fileName.contains("生成字词卡片音频")){
			insertLock.lock();//----------------------修改点---加锁
			HashMap<Object, Object> result;
			try {
				TeachExerciseFile exerciseFile = new TeachExerciseFile();
				exerciseFile.setPath(fileName);
				//文件名称
				exerciseFile.setName(fileName.substring(fileName.lastIndexOf("/"), fileName.lastIndexOf(".")));
				//文件后缀
				String extend = fileName.substring(fileName.lastIndexOf(".") + 1);
				exerciseFile.setExt(extend);
				teachExerciseFileMapper.insert(exerciseFile);
				result = new HashMap<>();
				result.put("file_id", exerciseFile.getId());
				result.put("file_url", exerciseFile.getPath());
			} finally {
				insertLock.unlock(); //----------------------修改点---释放锁
			}
			return R.ok().setCode(CommonConstants.SUCCESS).setData(result);
		}
		return R.ok().setCode(CommonConstants.SUCCESS).setData(fileName);
	}
	return R.ok().setCode(CommonConstants.FAIL);
}

5. 结束

解决高并发重复键问题的核心在于 保证操作的原子性 。优先通过数据库约束拦截冲突,结合业务场景选择锁机制或重试策略。实际开发中需权衡性能与一致性需求。

任何疑问,欢迎私信指教!!!
分享:
对付敌人我们需要很大的勇气,但在朋友面前坚定立场需要更大的勇气。------阿不思 邓布利多

相关推荐
风雅的远行者1 分钟前
mysql互为主从失效,重新同步
数据库·mysql
晨岳9 分钟前
CentOS 安装 JDK+ NGINX+ Tomcat + Redis + MySQL搭建项目环境
java·redis·mysql·nginx·centos·tomcat
执笔诉情殇〆16 分钟前
前后端分离(java) 和 Nginx在服务器上的完整部署方案(redis、minio)
java·服务器·redis·nginx·minio
YuTaoShao20 分钟前
【LeetCode 热题 100】24. 两两交换链表中的节点——(解法一)迭代+哨兵
java·算法·leetcode·链表
程序员的世界你不懂42 分钟前
(20)Java+Playwright自动化测试- 操作鼠标拖拽 - 上篇
java·python·计算机外设
AI360labs_atyun1 小时前
Java在AI时代的演进与应用:一个务实的视角
java·开发语言·人工智能·科技·学习·ai
宇钶宇夕1 小时前
S7-1200 系列 PLC 中 SCL 语言的 PEEK 和 POKE 指令使用详解
运维·服务器·数据库·程序人生·自动化
绿蚁新亭1 小时前
Spring的事务控制——学习历程
数据库·学习·spring
不像程序员的程序媛2 小时前
redis的一些疑问
java·redis·mybatis
知其然亦知其所以然2 小时前
Java 面试高频题:GC 到底回收了什么、怎么回收、啥时候回收?
java·后端·面试