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

JPA乐观锁未抛出OptimisticLockingException问题咨询

JPA乐观锁在并发删除场景下未触发异常反而插入数据的问题

问题背景

我给JPA实体添加了乐观锁字段:

@Version
var version: Long? = null

模拟并发更新与删除的测试中,预期触发OptimisticLockingException,前置条件是数据库中有2条数据。测试代码如下:

@Transactional
fun update(id: String){
    val existing = repository.findById(id)
    // 模拟另一个事务删除ID为2的条目
    // some concurrent code execution deleting the entry with id 2 in different transaction

    // 修改ID为2的条目
    repo.saveAll(existing)
}

实际调试发现,数据库中ID为2的条目已被并发删除,但调用saveAll时并未抛出预期异常,反而重新插入修改后的条目,覆盖了删除操作。

问题根源

JPA乐观锁依赖版本字段校验,但saveAll的执行逻辑是:

  • 若实体是托管状态且数据库中存在对应记录,执行UPDATE语句,此时会带上版本字段条件,不匹配则抛出OptimisticLockingException。
  • 若数据库中已无对应记录,JPA会将该实体判定为新实体,执行INSERT操作,完全跳过乐观锁校验。

当并发事务删除记录后,当前事务中的实体虽然是之前查询得到的托管对象,但数据库中已无匹配记录,因此触发了插入逻辑而非更新逻辑。

解决方案

1. 保存前显式校验实体存在性

在执行saveAll前,逐个检查实体是否仍存在于数据库,不存在则主动抛出异常:

@Transactional
fun update(id: String){
    val existingEntities = repository.findById(id)
    // 模拟并发删除操作

    // 校验每个实体是否存在
    existingEntities.forEach { entity ->
        if (!repository.existsById(entity.id)) {
            throw OptimisticLockingException("Entity with ID ${entity.id} was deleted by another transaction")
        }
    }

    // 修改实体属性
    existingEntities.find { it.id == "2" }?.apply {
        // 执行修改逻辑
    }

    repo.saveAll(existingEntities)
}

2. 使用JPQL自定义更新语句

通过自定义带版本和ID条件的更新语句,利用返回的受影响行数判断操作是否成功:

// 在Repository接口中定义方法
@Modifying
@Query("UPDATE YourEntity e SET e.yourField = :newValue, e.version = e.version + 1 WHERE e.id = :entityId AND e.version = :currentVersion")
fun updateWithLockCheck(@Param("entityId") id: String, @Param("newValue") value: String, @Param("currentVersion") version: Long): Int

// 业务方法中调用
@Transactional
fun update(id: String){
    val existingEntity = repository.findById("2").orElseThrow()
    // 模拟并发删除操作

    // 执行更新并检查受影响行数
    val affectedRows = repository.updateWithLockCheck(existingEntity.id, "updatedValue", existingEntity.version!!)
    if (affectedRows == 0) {
        throw OptimisticLockingException("Entity was deleted or modified by another transaction")
    }
}

这种方式直接在数据库层面执行带版本校验的更新,若记录已被删除,受影响行数为0,此时可抛出异常。

3. 利用EntityManager进行状态校验

通过EntityManager查询数据库最新状态,对比版本和存在性:

@Autowired
private lateinit var entityManager: EntityManager

@Transactional
fun update(id: String){
    val existingEntities = repository.findById(id)
    // 模拟并发删除操作

    existingEntities.forEach { entity ->
        val dbEntity = entityManager.find(entity::class.java, entity.id)
        when {
            dbEntity == null -> throw OptimisticLockingException("Entity deleted concurrently")
            dbEntity.version != entity.version -> throw OptimisticLockingException("Entity version mismatch")
            else -> {
                // 将修改后的属性复制到dbEntity(保持托管状态)
                dbEntity.yourField = entity.yourField
            }
        }
    }
    entityManager.flush()
}

EntityManager.find会直接查询数据库最新状态,若记录不存在则返回null,可直接抛出异常;若存在则对比版本,不一致也抛出异常。

内容的提问来源于stack exchange,提问作者midnight

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:55:08