Spring Data JDBC执行插入时为何会触发SELECT语句?
问题描述
在Spring应用中遇到了一个令人困惑的死锁问题:无活跃事务的情况下向表中插入数据时触发了死锁。使用的是SimpleJdbcRepository的save方法,并非寻求死锁解决方案,而是想了解触发原因。
猜测可能是save方法启动了事务、执行了至少一条SELECT语句并获取锁,进而导致死锁,但插入操作本不应执行SELECT。此外无法在本地复现该问题,JDBC日志仅显示插入语句。
疑问:
- Spring Data JDBC为何会执行这些额外的SELECT语句?
- 如何消除这些语句?
以下是死锁异常的堆栈跟踪:
org.postgresql.util.PSQLException: ERROR: deadlock detected Detail: Process 315008 waits for ShareLock on transaction 11699360; blocked by process 315268. Process 315268 waits for ShareLock on transaction 11699361; blocked by process 315008. Hint: See server log for query details. Where: while locking tuple (5,18) in relation "journey" SQL statement "SELECT 1 FROM ONLY "my_schema"."user" x WHERE "id" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x" at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2676) at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2366) at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:356) at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:496) at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:413) at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:190) at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:152) at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) at org.springframework.jdbc.core.JdbcTemplate.lambda$update$3(JdbcTemplate.java:992) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651) at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:991) at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.update(NamedParameterJdbcTemplate.java:356) at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.update(NamedParameterJdbcTemplate.java:340) at org.springframework.data.jdbc.core.convert.IdGeneratingInsertStrategy.execute(IdGeneratingInsertStrategy.java:67) at org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy.insert(DefaultDataAccessStrategy.java:117) at org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.executeInsertRoot(JdbcAggregateChangeExecutionContext.java:77) at org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:66) at org.springframework.data.jdbc.core.AggregateChangeExecutor.lambda$execute$0(AggregateChangeExecutor.java:52) at java.base/java.util.ArrayList.forEach(Unknown Source) at org.springframework.data.relational.core.conversion.DefaultAggregateChange.forEachAction(DefaultAggregateChange.java:127) at org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:52) at org.springframework.data.jdbc.core.JdbcAggregateTemplate.store(JdbcAggregateTemplate.java:360) at org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:161) at org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:78)
原因分析与解决方案
原因分析
这条SELECT ... FOR KEY SHARE语句并非Spring Data JDBC主动执行,而是PostgreSQL为维护外键约束自动触发的:
- 你插入的
journey表存在指向user表的外键,插入journey记录时,PostgreSQL需要验证关联的user记录是否存在,同时为了防止验证后到插入完成前该user记录被删除或修改主键,会自动加上FOR KEY SHARE锁。 - JDBC日志仅显示插入语句是因为这条SELECT是数据库内部触发的,不属于应用层执行的SQL。
- Spring Data JDBC的
save方法默认会启动隐式事务(JDBC操作本身默认是事务性的),这导致锁等待发生在事务级别,进而引发死锁。
消除额外SELECT语句的方法
- 移除不必要的外键约束:如果
journey表的外键不是业务必需的,直接删除该约束,PostgreSQL就不会触发这条验证用的SELECT。 - 调整锁获取顺序:确保所有并发插入请求以相同顺序获取
user记录的锁(比如按user.id升序处理),避免循环等待导致死锁。 - 显式控制事务:将插入操作包裹在显式事务中,根据业务场景调整事务隔离级别或锁策略,减少锁冲突概率。
内容的提问来源于stack exchange,提问作者findusl
相关产品推荐
相关产品推荐

