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

使用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
}

原因分析

  1. 隔离级别与悲观锁的冲突:你使用了REPEATABLE READ隔离级别,PostgreSQL在这个级别下的并发控制逻辑会严格检测序列化冲突。虽然PESSIMISTIC_WRITE会给查询的行加锁,但在REPEATABLE READ下,当多个事务同时竞争同一行锁时,PostgreSQL不会自动等待锁释放,而是直接抛出序列化失败的异常——这和你预期的“等待重试”行为不符。
  2. 事务执行顺序问题:即使加了悲观锁,若两个事务的执行路径导致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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 17:23:19