Spring JPA事务中并发请求重复记录问题的解决方案咨询
Spring JPA 并发场景下实现“存在则复用,不存在则创建”的正确姿势
针对你遇到的并发问题,核心原因是先查询再插入的操作不是原子性的,并发请求会同时通过“不存在”的检查,导致唯一约束冲突。以下是几种可靠的解决方案,按推荐优先级排序:
1. 捕获唯一约束异常,回退查询(通用方案,高并发友好)
利用数据库唯一约束的原子性,直接尝试插入记录——成功则返回新记录,失败(唯一约束冲突)则查询已存在的记录复用。这种方式不依赖特定数据库,是最通用的并发安全方案。
代码示例:
@Transactional public YourEntity getOrCreate(Long id) { try { YourEntity newEntity = new YourEntity(); newEntity.setId(id); // 填充其他业务属性 return yourEntityRepository.save(newEntity); } catch (PersistenceException e) { // 校验是否为唯一约束冲突异常(不同数据库异常类型/错误码不同) if (isUniqueKeyViolation(e)) { // 异常后查询已存在的记录 return yourEntityRepository.findById(id) .orElseThrow(() -> new RuntimeException("无法获取目标记录")); } // 非唯一约束异常,正常抛出 throw e; } } // 辅助方法:判断是否为唯一约束冲突(以MySQL为例) private boolean isUniqueKeyViolation(PersistenceException e) { Throwable rootCause = e.getCause(); return rootCause instanceof SQLIntegrityConstraintViolationException && ((SQLIntegrityConstraintViolationException) rootCause).getErrorCode() == 1062; }
说明
- 数据库的唯一约束是最可靠的并发屏障,确保同一ID只能插入一次
- 事务保证插入失败时不会留下脏数据,回退后查询的是已提交的有效记录
- 注意:不同数据库的唯一约束异常类型和错误码不同,需要根据你使用的数据库调整判断逻辑(比如PostgreSQL的错误码是23505)
2. MySQL专属:使用INSERT ... ON DUPLICATE KEY UPDATE
如果你的数据库是MySQL,可以直接用原生SQL实现原子性的“存在则忽略,不存在则插入”,避免异常捕获的开销。
代码示例:
// Repository层定义原生SQL方法 @Repository public interface YourEntityRepository extends JpaRepository<YourEntity, Long> { @Modifying @Transactional @Query(value = "INSERT INTO your_entity (id, name, status) VALUES (:id, :name, :status) " + "ON DUPLICATE KEY UPDATE id = id", // 冲突时不做任何更新,仅触发约束检查 nativeQuery = true) void insertOrIgnore(@Param("id") Long id, @Param("name") String name, @Param("status") Integer status); } // 服务层调用 @Transactional public YourEntity getOrCreate(Long id, String name, Integer status) { yourEntityRepository.insertOrIgnore(id, name, status); // 插入或忽略后,直接查询即可拿到目标记录 return yourEntityRepository.findById(id) .orElseThrow(() -> new RuntimeException("记录获取失败")); }
说明
ON DUPLICATE KEY UPDATE id = id是一个空操作,仅利用MySQL的唯一约束逻辑判断记录是否存在- 整个操作是数据库层面的原子操作,完全避免并发问题,性能比捕获异常更好
3. 悲观锁方案(适合低并发场景)
通过JPA的悲观锁机制,在查询时锁住目标记录,确保同一时间只有一个请求能执行“查询-插入”逻辑。
代码示例:
@Transactional public YourEntity getOrCreate(Long id) { // 加悲观写锁,其他请求必须等待当前事务释放锁才能查询 YourEntity existing = yourEntityRepository.findById(id, LockModeType.PESSIMISTIC_WRITE).orElse(null); if (existing != null) { return existing; } // 不存在则创建 YourEntity newEntity = new YourEntity(); newEntity.setId(id); // 填充属性 return yourEntityRepository.save(newEntity); }
说明
- 悲观锁会降低并发性能,因为其他请求需要等待锁释放,适合并发量不高的场景
- 注意锁的粒度,避免锁住过多数据影响整体性能
内容的提问来源于stack exchange,提问作者Bilgehan
相关产品推荐
相关产品推荐

