SpringBoot数据库锁机制实现:并发下重复插入记录问题排查
解决并发场景下「记录不存在才插入」的重复插入问题及SpringBoot数据库锁正确实现
问题原因分析
你当前代码出现重复插入的核心问题有两个:
- 事务边界不合理:
method1和method2都标记了@Transactional,导致saveSlot的事务被合并到上层事务中,插入操作的提交被延迟到sleep(1000)之后。此时另一个线程的查询在事务隔离级别下无法看到未提交的记录,仍会执行插入。 - 悲观锁的局限性:
@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
相关产品推荐
相关产品推荐

