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

Spring Data JPA的JpaRepository.saveAll()方法并发修改异常问题

嘿,咱们来好好捋一捋你用Spring Boot + JPA保存关联实体时遇到的并发修改异常问题。我仔细看了你的实体类和测试代码,下面分析下可能的原因,还有对应的解决办法:

可能的原因分析

1. 双向关联的重复维护+全级联操作,触发并发冲突

你的几个双向关联(多对多的Employee-Community、一对多的Department-Employee、一对一的Employee-Address)都加了cascade=CascadeType.ALL,而且测试代码里还同时维护了关联的两端——比如:

  • 多对多场景:你既给comm0.setListOfEmployees(lemp),又给emp0.setListOfCommunities(lcomm),这会让Hibernate在持久化时对中间表community_employee_mapping执行多次插入/更新操作。并发情况下,多个线程操作同一关联数据,很容易引发锁冲突或者数据不一致。
  • 一对多场景:dept0.setLemp(lemp)和emp0.setDept(dept0)同时维护,导致Hibernate重复更新Employee的dept_id字段,进一步加剧并发冲突。

双向关联里,通常只需要在拥有方(也就是定义外键/中间表的那一方)维护关联,另一方(标注mappedBy的一方)不需要主动设置,否则会产生大量重复SQL,把并发问题放大。

2. 主键生成策略的并发瓶颈

Address实体用了GenerationType.TABLE作为主键生成方式,这种策略依赖一张共享的主键生成表,高并发下大量线程同时抢主键,会产生严重的锁竞争,甚至出现主键冲突,直接触发并发异常。

其他实体用的GenerationType.AUTO在PostgreSQL下默认是SEQUENCE,虽然比TABLE策略好,但如果并发量极高,SEQUENCE步长设置不合理也可能出现短暂冲突,但TABLE的问题是最突出的。

3. 测试代码的潜在问题

你的测试循环创建了5万组实体,但看起来没调用erepo.save()来持久化?如果实际是多线程执行这个测试(比如用并发框架),大量线程同时执行级联保存,会让数据库的锁竞争直接拉满。另外,循环里的对象可能存在交叉引用(比如同一个Community被多个Employee关联),并发保存时会对同一Community记录或中间表行进行修改,触发乐观锁/悲观锁异常。

对应的解决办法

1. 优化双向关联的维护逻辑与级联配置

  • 多对多关联:只在拥有方(Employee,因为它定义了@JoinTable)保留cascade=CascadeType.ALL,另一方(Community)移除级联配置,并且测试代码里只维护Employee的关联(也就是只调用emp0.setListOfCommunities(lcomm),不用管comm0.setListOfEmployees(lemp))。
    修改Community类的关联代码:
@ManyToMany(mappedBy = "listOfCommunities") // 移除cascade=CascadeType.ALL
private List<Employee> listOfEmployees = new ArrayList<>();
  • 一对多关联:Employee是拥有方(因为它持有Department的外键),所以Department的@OneToMany去掉级联配置,测试代码里只维护Employee的setDept(),不用管Department的setLemp()。
    修改Department类的关联代码:
@OneToMany(mappedBy = "dept") // 移除cascade=CascadeType.ALL
private List<Employee> lemp = new ArrayList<>();
  • 一对一关联:同样,Employee是拥有方,Address的@OneToOne去掉级联配置,测试代码里只维护Employee的setAddress(),不用管Address的setEmp()。

2. 调整主键生成策略

把Address的主键生成策略从GenerationType.TABLE换成PostgreSQL支持的GenerationType.IDENTITY或者GenerationType.SEQUENCE:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 推荐用IDENTITY,或者SEQUENCE
private int add_id;

如果用SEQUENCE,可以设置合适的步长(比如@SequenceGenerator(name = "address_seq", sequenceName = "address_seq", allocationSize = 50)),减少数据库交互次数,缓解并发压力。

3. 优化并发保存逻辑

  • 如果是多线程测试,用批量保存代替单条保存,Spring Data JPA的saveAll()可以批量处理,减少数据库连接和锁竞争:
    List<Employee> employees = new ArrayList<>();
    // 循环内创建Employee对象并添加到list
    employees.add(emp0);
    employees.add(emp1);
    employees.add(emp2);
    // 循环结束后批量保存
    erepo.saveAll(employees);
    
  • 高并发场景下,用乐观锁保护实体:在实体类中添加@Version字段,Hibernate会自动维护版本号,并发修改时会检测版本号,避免脏数据:
    @Version
    private int version;
    
    这样并发修改同一实体时会抛出OptimisticLockingFailureException,你可以在业务代码里捕获并处理(比如重试)。
  • 尽量避免在并发场景下修改同一关联数据(比如同一个Community被多个线程同时关联到不同Employee),可以提前初始化好Community数据,或者用悲观锁(但悲观锁会影响性能,谨慎使用)。

4. 修复测试代码的遗漏

你的测试代码最后没调用保存方法,记得补上批量保存的逻辑,不然创建的对象根本不会持久化到数据库里。

额外的优化建议
  • 把Hibernate的刷新模式改成COMMIT(配置spring.jpa.properties.hibernate.flushMode=COMMIT),避免持久化过程中频繁刷新到数据库,减少锁竞争。
  • 调整PostgreSQL的数据库参数,比如增大max_connections、调整锁超时时间等,适应高并发场景。
  • 大量数据插入时,考虑用原生SQL或者批量插入的方式,绕过JPA的级联操作,既提升性能又减少并发冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:25:27