使用LockModeType.PESSIMISTIC_WRITE时PostgreSQL并发更新序列化错误
PostgreSQL并发更新报错:could not serialize access due to concurrent update 问题解决
问题背景
我开发了一个基于Spring Data JPA和PostgreSQL的应用,核心逻辑是从数据库查询实体、按业务需求修改后再保存回库。但当多个请求同时调用该功能时,会周期性抛出如下错误:
org.postgresql.util.PSQLException: ERROR: could not serialize access due to concurrent update 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.executeQuery(PgPreparedStatement.java:134) at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52) ... at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) at jdk.proxy2/jdk.proxy2.$Proxy255.findForUpdateByUserId(Unknown Source) at com.life.adapter.out.jpa.JpaStatusPortImpl.findForUpdateByUserId(JpaStatusPortImpl.kt:25)
我知道这个错误和仓库方法上的@Lock(LockModeType.PESSIMISTIC_WRITE)注解有关,但搞不懂具体原因。加了这个锁的事务难道不该等前一个事务完成后重试吗?请问怎么避免这个错误?
相关代码
仓库代码:
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Lock import org.springframework.stereotype.Repository import javax.persistence.LockModeType @Repository interface StatusRepository : JpaRepository<StatusEntity, UUID> { @Lock(LockModeType.PESSIMISTIC_WRITE) fun findForUpdateByUserId(userId: String): StatusEntity? }
业务方法逻辑:
@Transactional(isolation = Isolation.REPEATABLE_READ) fun upsert(userId: String) { val found = statusRepository.findForUpdateByUserId(userId) // 从数据库查询实体 // 业务逻辑处理 val saved = statusRepository.saveAndFlush(found) // 将修改后的实体保存回库 return saved }
原因分析
- 隔离级别与悲观锁的冲突:你使用了
REPEATABLE READ隔离级别,PostgreSQL在这个级别下的并发控制逻辑会严格检测序列化冲突。虽然PESSIMISTIC_WRITE会给查询的行加锁,但在REPEATABLE READ下,当多个事务同时竞争同一行锁时,PostgreSQL不会自动等待锁释放,而是直接抛出序列化失败的异常——这和你预期的“等待重试”行为不符。 - 事务执行顺序问题:即使加了悲观锁,若两个事务的执行路径导致PostgreSQL判定可能出现数据不一致,也会触发该错误,而非进入锁等待队列。
解决方案
1. 调整事务隔离级别为READ COMMITTED
PostgreSQL默认的READ COMMITTED隔离级别下,悲观锁的行为更符合预期:当一个事务持有行锁时,其他事务会等待锁释放,而非直接抛出异常。修改业务方法的事务注解即可:
@Transactional(isolation = Isolation.READ_COMMITTED) fun upsert(userId: String) { // 原有业务逻辑不变 }
2. 配置锁等待超时(保留REPEATABLE READ时使用)
如果你必须保留REPEATABLE READ隔离级别,可以给悲观锁添加超时配置,让事务等待锁释放一段时间,超时后才报错:
import javax.persistence.QueryHint import org.springframework.data.jpa.repository.QueryHints @Repository interface StatusRepository : JpaRepository<StatusEntity, UUID> { @Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints(value = [QueryHint(name = "javax.persistence.lock.timeout", value = "5000")]) // 等待5秒 fun findForUpdateByUserId(userId: String): StatusEntity? }
3. 添加并发冲突重试机制
极端情况下仍可能出现冲突,此时可以给业务方法添加重试逻辑,用Spring的@Retryable实现:
import org.springframework.retry.annotation.Backoff import org.springframework.retry.annotation.Retryable import org.postgresql.util.PSQLException @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(value = [PSQLException::class], maxAttempts = 3, backoff = Backoff(delay = 100)) fun upsert(userId: String) { // 原有业务逻辑不变 }
需要在Spring配置类上添加@EnableRetry注解开启重试功能。
4. 改用数据库原生UPSERT操作(推荐)
放弃“先查后改”的模式,直接用PostgreSQL原生的UPSERT语句(INSERT ... ON CONFLICT ... DO UPDATE),在数据库层面原子性完成插入或更新,从根源避免并发问题:
import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param @Repository interface StatusRepository : JpaRepository<StatusEntity, UUID> { @Query(value = """ INSERT INTO status (user_id, field1, field2) VALUES (:userId, :field1, :field2) ON CONFLICT (user_id) DO UPDATE SET field1 = EXCLUDED.field1, field2 = EXCLUDED.field2 RETURNING * """, nativeQuery = true) fun upsertByUserId( @Param("userId") userId: String, @Param("field1") field1: String, @Param("field2") Int ): StatusEntity? }
这种方式无需加悲观锁,性能更高且一致性更可靠。
内容的提问来源于stack exchange,提问作者NeverSleeps
相关产品推荐
相关产品推荐

