You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot数据库锁机制实现:并发下重复插入记录问题排查

解决并发场景下「记录不存在才插入」的重复插入问题及SpringBoot数据库锁正确实现

问题原因分析

你当前代码出现重复插入的核心问题有两个:

  1. 事务边界不合理:method1和method2都标记了@Transactional,导致saveSlot的事务被合并到上层事务中,插入操作的提交被延迟到sleep(1000)之后。此时另一个线程的查询在事务隔离级别下无法看到未提交的记录,仍会执行插入。
  2. 悲观锁的局限性:@Lock(LockModeType.PESSIMISTIC_WRITE)对应的SELECT ... FOR UPDATE仅对已存在的行生效;当记录不存在时,若数据库隔离级别未配置正确,无法触发间隙锁阻塞并发插入。

解决方案

1. 数据库唯一约束(兜底方案)

这是最可靠的防重复手段,即使代码锁失效,数据库层面也会阻止重复插入:

  • 给slot表的time字段添加唯一索引:
    ALTER TABLE slot ADD UNIQUE INDEX idx_time (time);
    
  • 代码中捕获唯一键冲突异常,处理重复场景:
    try {
        slotRepo.save(newSlot);
    } catch (DataIntegrityViolationException e) {
        System.out.println("时段 " + newTime + " 已被占用");
    }
    

2. 调整事务边界+正确使用悲观锁

确保「查询-插入」操作在独立事务中执行,利用数据库间隙锁阻塞并发请求:

  • 移除method1和method2的@Transactional注解,让saveSlot的事务独立控制提交时机
  • 保留saveSlot的@Transactional,确保加锁查询和插入在同一个事务内

修改后的Service层代码:

public void method1() throws InterruptedException {
    saveSlot("sam", "07:00");
    Thread.sleep(1000); 
}

public void method2() throws InterruptedException {
    saveSlot("Curran", "07:00");
    Thread.sleep(1000); 
}

@Transactional(rollbackFor = Exception.class)
public void saveSlot(String name, String newTime) {
    LocalTime time = LocalTime.parse(newTime);     
    Optional<Slot> slotOptional = slotRepo.findByTimeWithLock(time);
   
    if (!slotOptional.isPresent()) {
        Slot newSlot = new Slot();
        newSlot.setName(name);
        newSlot.setTime(time);
        try {
            slotRepo.save(newSlot);
        } catch (DataIntegrityViolationException e) {
            System.out.println("时段 " + newTime + " 已被占用");
        }
    }
}

3. 确保数据库隔离级别

MySQL InnoDB默认隔离级别为REPEATABLE READ,该级别下SELECT ... FOR UPDATE会对不存在的行添加间隙锁,阻止其他线程插入相同范围的记录,这是悲观锁生效的前提。


SpringBoot中数据库锁的正确实现

悲观锁

  • 适用场景:并发冲突频繁的场景,如秒杀、预约系统
  • 实现方式:在Repository查询方法上添加@Lock(LockModeType.PESSIMISTIC_WRITE),对应数据库SELECT ... FOR UPDATE语句
  • 注意事项:
    • 必须在事务内执行加锁查询,否则锁不会生效
    • 不同数据库的锁行为有差异,需适配数据库特性(如InnoDB间隙锁)

乐观锁

  • 适用场景:并发冲突较少的场景
  • 实现方式:在实体类的版本字段上添加@Version注解,Hibernate会自动校验版本号
  • 示例:
    @Entity
    public class Slot {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private LocalTime time;
        @Version
        private Integer version; // 版本字段
    }
    
    更新时若版本号不匹配,会抛出OptimisticLockingFailureException

唯一约束

  • 核心作用:作为兜底机制,无论代码层面是否有锁,数据库都会阻止重复数据插入
  • 实现方式:在表的唯一标识字段上添加唯一索引或约束

内容的提问来源于stack exchange,提问作者ASIF KAIF

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 09:02:02