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

基于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的组合:

  1. 获取锁(设置合理的超时时间,避免死锁)
  2. 查询Host是否存在
  3. 不存在则创建,存在则直接返回
  4. 释放锁

不过这种方案会增加系统复杂度,需要处理锁超时、锁竞争等问题,只适合必须跨节点保证唯一性的场景。

总结建议

  1. 如果数据库支持原生的冲突处理语法,优先选择数据库原生方案,这是最优解;
  2. 如果无法使用原生SQL,方案1是不错的选择,但要做好异常的精确校验和事务独立性的考量;
  3. 方案2尽量避免,除非你的系统并发量极低,否则性能问题会非常突出。

内容的提问来源于stack exchange,提问作者Andrey Yaskulsky

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 03:59:44