基于Spring Data实现并发环境下的findOrCreate策略
关于并发场景下Host实体findOrCreate方法的实现建议
我之前处理过几乎一模一样的并发创建唯一约束实体的场景,来聊聊对这两种方案的看法,以及一些替代思路:
对你提出的两种方案的分析
方案1:RequiresNew事务+冲突捕获
这个方案的性能优势确实很明显——毕竟在大部分没有冲突的场景下,直接插入就能完成操作,只有当并发冲突发生时才会走查询逻辑,吞吐量比串行化隔离级别高很多。不过需要注意几个潜在的坑:
- 异常捕获的精确性:你catch的
ConstraintViolationException可能来自其他约束(比如如果id字段也有唯一约束的话),所以最好在捕获后校验异常对应的约束名称是不是你定义的uq_host_0,避免误处理其他冲突场景。 - 事务独立性:
create方法用了RequiresNew,意味着它会开启一个独立的事务并提交,和外层findOrCreate的事务完全隔离。如果你的业务逻辑中findOrCreate还有其他关联操作,要注意这种独立提交会不会破坏业务一致性。 - 数据库差异:不同数据库对唯一约束冲突抛出的异常类型可能略有不同,比如有些数据库会抛出
SQLIntegrityConstraintViolationException而非JPA的ConstraintViolationException,需要确保异常捕获覆盖到实际情况。
方案2:Serializable隔离级别下先查后插
这个方案的逻辑非常直观,串行化隔离级别会强制事务按顺序执行,从根本上避免了并发冲突。但它的缺点也很致命:
- 性能瓶颈:Serializable隔离级别会大幅降低数据库的并发能力,尤其是在高并发场景下,会出现大量的锁等待,吞吐量急剧下降。
- 数据库实现差异:不同数据库对Serializable的实现逻辑不一样,比如MySQL会直接锁表,而PostgreSQL用的是快照隔离的Serializable,可能会出现一些意想不到的锁行为,增加排查难度。
更推荐的替代方案
利用数据库原生原子操作(优先推荐)
几乎所有主流数据库都支持原生的“插入冲突则忽略/更新”语法,这是数据库层面的原子操作,完全避免了应用层的并发问题,性能也是最优的。
比如针对MySQL,可以用INSERT ... ON DUPLICATE KEY UPDATE:
@Repository public interface HostRepository extends JpaRepository<Host, String> { @Query(value = "INSERT INTO host (org_name, host_name) VALUES (:orgName, :hostName) ON DUPLICATE KEY UPDATE id = id", nativeQuery = true) @Modifying void insertOrIgnore(@Param("orgName") String orgName, @Param("hostName") String hostName); Host findByOrgNameAndHostName(String orgName, String hostName); }
然后在服务层实现:
@Service public class HostService { @Autowired private HostRepository hostRepository; @Transactional public Host findOrCreate(Host host) { // 原子操作:存在则忽略,不存在则插入 hostRepository.insertOrIgnore(host.getOrgName(), host.getHostName()); // 查询最终存在的记录 return hostRepository.findByOrgNameAndHostName(host.getOrgName(), host.getHostName()); } }
如果是PostgreSQL,可以用INSERT ... ON CONFLICT DO NOTHING:
INSERT INTO host (org_name, host_name) VALUES (:orgName, :hostName) ON CONFLICT (org_name, host_name) DO NOTHING
这种方案不需要处理异常,也不需要依赖事务隔离级别或嵌套事务,完全由数据库保证原子性,是最可靠且高效的方式。
分布式锁(跨实例/跨数据库场景)
如果你的系统是跨多个数据库实例部署的(单数据库场景没必要),可以用Redis或ZooKeeper实现分布式锁,锁的key用orgName+hostName的组合:
- 获取锁(设置合理的超时时间,避免死锁)
- 查询Host是否存在
- 不存在则创建,存在则直接返回
- 释放锁
不过这种方案会增加系统复杂度,需要处理锁超时、锁竞争等问题,只适合必须跨节点保证唯一性的场景。
总结建议
- 如果数据库支持原生的冲突处理语法,优先选择数据库原生方案,这是最优解;
- 如果无法使用原生SQL,方案1是不错的选择,但要做好异常的精确校验和事务独立性的考量;
- 方案2尽量避免,除非你的系统并发量极低,否则性能问题会非常突出。
内容的提问来源于stack exchange,提问作者Andrey Yaskulsky
相关产品推荐
相关产品推荐

