Spring Boot 3升级:并发插入MariaDB时出现JPA死锁问题
Spring Boot 3.1升级后JPA批量插入死锁问题排查与解决
不少开发者在从Spring Boot 2.x升级到3.x(搭配Hibernate 6.x)后,遇到过类似的批量插入死锁问题,尤其是MySQL/MariaDB环境下,之前稳定的代码突然出现高频死锁,以下是针对这类问题的分析和解决思路:
可能的原因
- Hibernate 6批量插入逻辑变更:Spring Boot 3.x默认适配Hibernate 6,它的JDBC批量处理实现(比如
MutationExecutorSingleBatched)和Hibernate 5有明显差异,批量插入时锁的获取顺序、时机都可能变化,容易引发循环等待型死锁。 - 索引锁竞争:你提到的表有外键索引和状态索引,InnoDB在插入时会对涉及的索引加意向锁/排他锁,如果多个并行事务的插入操作涉及的索引键顺序不一致,就会形成死锁的必要条件——循环等待。
- 事务Flush时机调整:Spring Boot 3对JPA事务的默认Flush策略可能有微调,自动Flush的触发更频繁,导致多个事务同时执行批量插入操作,加剧锁竞争。
可行的解决办法
- 调整Hibernate批量配置
- 降低批量大小:在
application.properties中设置spring.jpa.properties.hibernate.jdbc.batch_size=10,减少单次批量操作涉及的数据量,缩小锁的覆盖范围。 - 开启有序插入:添加
spring.jpa.properties.hibernate.order_inserts=true,让Hibernate按固定顺序(比如主键、索引字段)排序插入语句,确保所有事务的锁获取顺序一致,从根源避免循环等待。 - 禁用版本数据批量处理:如果你的实体没有使用乐观锁,可以设置
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=false。
- 降低批量大小:在
- 优化表索引
- 检查外键关联的表是否有长时间持锁的操作,避免外键触发的连锁锁竞争;如果状态索引的基数极低(比如只有2-3种状态),可以临时移除该索引测试,确认是否是索引导致的锁冲突。
- 添加事务重试机制
- 死锁属于临时性异常,重试通常可以解决。用Spring Retry实现重试逻辑:
@Retryable(value = {LockAcquisitionException.class, SQLTransactionRollbackException.class}, maxAttempts = 3, backoff = @Backoff(delay = 100)) @Transactional public PurchaseOrder savePurchaseOrder(PurchaseOrder order) { return purchaseOrderRepository.save(order); }
- 死锁属于临时性异常,重试通常可以解决。用Spring Retry实现重试逻辑:
- 调整测试并发策略
- 如果死锁仅在测试环境出现,检查测试用例是否有大量并行插入该表的场景,降低测试并发度,或者对冲突的测试用例设置顺序执行,减少竞争。
异常栈信息
Caused by: org.hibernate.exception.LockAcquisitionException: could not execute batch [(conn=1247) Deadlock found when trying to get lock; try restarting transaction] [insert into `order_purchase_order` (`buyer_purchase_order_comment`,`created_at`,`current_state`,`last_modified_at`,`order_id`,`state_history`,`supplier_dialect_id`,`supplier_id`,`supplier_id_name`,`supplier_name`,`supplier_sales_order_reference`,`id`) values (?,?,?,?,?,?,?,?,?,?,?,?)] at org.hibernate.dialect.MySQLDialect.lambda$buildSQLExceptionConversionDelegate$3(MySQLDialect.java:1178) at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:56) at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:108) at org.hibernate.engine.jdbc.batch.internal.BatchImpl.lambda$performExecution$2(BatchImpl.java:289) at org.hibernate.engine.jdbc.mutation.internal.PreparedStatementGroupSingleTable.forEachStatement(PreparedStatementGroupSingleTable.java:59) at org.hibernate.engine.jdbc.batch.internal.BatchImpl.performExecution(BatchImpl.java:262) at org.hibernate.engine.jdbc.batch.internal.BatchImpl.execute(BatchImpl.java:241) at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.getBatch(JdbcCoordinatorImpl.java:173) at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleBatched.resolveBatch(MutationExecutorSingleBatched.java:47) at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleBatched.performBatchedOperations(MutationExecutorSingleBatched.java:60) at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:55) at org.hibernate.persister.entity.mutation.InsertCoordinator.doStaticInserts(InsertCoordinator.java:170) at org.hibernate.persister.entity.mutation.InsertCoordinator.coordinateInsert(InsertCoordinator.java:112) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2670) at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:102) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:618) at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:489) at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:486) at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:358) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1412) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:485) at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2296) at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1961) at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267) at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:561) ... 24 common frames omitted Caused by: java.sql.BatchUpdateException: (conn=1247) Deadlock found when trying to get lock; try restarting transaction at org.mariadb.jdbc.export.ExceptionFactory.createBatchUpdate(ExceptionFactory.java:215) at org.mariadb.jdbc.client.impl.StandardClient.executePipeline(StandardClient.java:637) at org.mariadb.jdbc.ClientPreparedStatement.executeBatchPipeline(ClientPreparedStatement.java:196) at org.mariadb.jdbc.ClientPreparedStatement.executeInternalPreparedBatch(ClientPreparedStatement.java:128) at org.mariadb.jdbc.ClientPreparedStatement.executeBatch(ClientPreparedStatement.java:466) at com.zaxxer.hikari.pool.ProxyStatement.executeBatch(ProxyStatement.java:127) at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeBatch(HikariProxyPreparedStatement.java) at org.hibernate.engine.jdbc.batch.internal.BatchImpl.lambda$performExecution$2(BatchImpl.java:275) ... 51 common frames omitted Caused by: java.sql.SQLTransactionRollbackException: (conn=1247) Deadlock found when trying to get lock; try restarting transaction at org.mariadb.jdbc.export.ExceptionFactory.createException(ExceptionFactory.java:296) at org.mariadb.jdbc.export.ExceptionFactory.create(ExceptionFactory.java:370) at org.mariadb.jdbc.message.ClientMessage.readPacket(ClientMessage.java:134) at org.mariadb.jdbc.client.impl.StandardClient.readPacket(StandardClient.java:872) at org.mariadb.jdbc.client.impl.StandardClient.readResults(StandardClient.java:811) at org.mariadb.jdbc.client.impl.StandardClient.readResponse(StandardClient.java:730) at org.mariadb.jdbc.client.impl.StandardClient.executePipeline(StandardClient.java:571) ... 57 common frames omitted
内容的提问来源于stack exchange,提问作者Matthias
相关产品推荐
相关产品推荐

