高并发下的重复记录之谜:从前端到数据库的全方位排查

在实际开发中,我们经常会遇到这样的场景:需要为某类业务生成唯一序列号(比如订单号、需求编号等),并且要求在高并发场景下保证唯一性。最近团队就遇到了一个典型问题:前端通过 for 循环快速提交POST请求时,后端生成的序列号出现了重复记录,且重复记录的year,month,day,flowCode完全相同。
我们从问题出发,深入分析背后的技术原理,理解为什么会出现重复、@Transactional注解为何 会“失效”,以及如何彻底解决这类问题。
一、现象回顾:重复记录是如何产生的?
先简单描述一下业务场景:后端有一个getAndIncrementSeq方法,用于根据year,month,day,flowCode(年月日 + 业务类型)生成自增序列号。逻辑是:
- 先查询数据库中是否存在该组合的记录;
- 若存在,则将
currentSeq字段 + 1 并更新; - 若不存在,则插入一条新记录(初始
currentSeq=1)。
前端在某个操作中,通过for循环连续提交了多个 POST 请求(比如一次性创建 5 条同类型记录),结果发现数据库中出现了两条year,month,day,flowCode完全相同的记录,导致序列号重复。
二、问题根源剖析:从并发到数据库的 “漏洞”
1. 前端快速提交:高并发的 “催化剂”
前端使用for循环提交POST请求时,会出现一个关键问题:请求之间几乎没有时间间隔。
- 正常情况下,用户手动操作的请求间隔较长,并发冲突概率低;
- 但
for循环提交会在毫秒级甚至更短的时间内触发多个同类型请求,后端会同时启动多个线程处理这批请求,将系统推入高并发场景。
这些线程会同时执行getAndIncrementSeq方法,为后续的冲突埋下伏笔。
2. @Transactional的 “局限性”:原子性≠并发安全性
后端方法上标注了@Transactional,但为什么还会出问题?
@Transactional的核心作用是保证事务内操作的原子性(要么全成功,要么全回滚),但它无法直接解决 “并发写入冲突”,原因有两点:
事务隔离级别的影响:数据库默认隔离级别(如PostgreSQL的READ COMMITTED)下,事务使用 “快照读”。假设线程 A 和线程 B 同时开始事务:
- 线程 A 查询
(year, month, day, flowCode),发现记录不存在; - 线程 B 在同一时间查询,由于线程 A 的插入操作尚未提交(事务未结束),线程 B 的快照中也看不到这条记录,同样认为 “记录不存在”。
- 线程 A 查询
并发可见性问题:事务的原子性仅保证 “操作不中断”,但不能强制让多个并发事务 “互相可见” 未提交的操作。因此,即使有
@Transactional,多个线程仍可能因 “查询盲区” 而重复执行插入逻辑。
3. 解决方法:构建唯一索引—— “最后一道防线”
在数据库表中(year, month, day, flowCode)字段上建立唯一索引。
若没有唯一索引:当线程 A 和线程 B 同时插入记录时,数据库不会对这四个字段的组合进行唯一性校验,两条完全相同的记录会被同时写入表中。
若有唯一索引:数据库会自动拦截重复插入操作(触发异常)。此时,若代码中实现了重试机制(如检测到唯一冲突后重新查询并更新),则后失败的线程会基于已存在的记录递增序列号,避免重复。
三、终极解决方案:三层防护机制
要彻底解决高并发下的重复记录问题,需要从 “前端控制 + 后端逻辑 + 数据库约束” 三个层面入手:
1. 前端:减少不必要的并发请求
- 尽量避免使用
for循环批量提交 POST 请求,改为通过一次性接口提交批量数据; - 若必须循环提交,可添加延迟(如
setTimeout),降低请求并发度。
2. 后端:完善并发控制逻辑
- 保留
@Transactional:保证 “查询 - 插入 / 更新” 操作的原子性,避免中间状态导致的高并发数据不一致; - 强化重试机制:针对唯一索引冲突异常,增加重试逻辑(如最多重试 3 次),让失败的线程重新执行查询 - 更新流程;
- 行锁控制:在查询时使用行锁,避免并发更新时的 “丢失更新” 的问题(适用于记录已存在的场景)。
3. 数据库:建立唯一索引,守住最后一道防线
最后的防线:建立唯一索引,强制数据库层面拒绝重复插入:有了唯一索引后,即使前端并发请求再多,后端事务隔离级别导致 “查询盲区”,数据库也会像一道 “防火墙”,拦截所有重复插入操作,配合后端重试机制即可保证序列号唯一。
四、总结:高并发下的数据唯一性保障
通过这个问题,我们可以总结出高并发场景下保证数据唯一性的核心原则:
- 数据库约束是底线:唯一索引 / 主键是防止重复的最后一道防线,不可省略;
- 事务是基础:保证操作原子性,仍需结合隔离级别理解其局限性;
- 重试机制是补充:针对并发冲突(如唯一索引冲突),通过重试让线程重新获取最新数据,避免重复;
- 前端优化是辅助:减少不必要的并发请求,降低前端重复请求压力。
希望以上能帮大家避开类似的坑,在高并发场景下写出更健壮的代码!
欢迎在评论区分享你的高并发踩坑经历,一起交流解决方案~
