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

