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

Spring Boot事务REPEATABLE_READ与SERIALIZABLE隔离级为何允许并发读?

核心认知偏差纠正

你对MySQL InnoDB隔离级别的实现逻辑存在两个关键误解:

  1. InnoDB的REPEATABLE READ隔离级别默认通过*多版本并发控制(MVCC)*实现,普通无锁SELECT走快照读逻辑,不会加任何读锁,多个并发事务可以同时读取同一行的快照版本,完全不会互相阻塞。只有显式声明加锁的SELECT(SELECT ... LOCK IN SHARE MODE/SELECT ... FOR UPDATE)才会触发加锁逻辑。
  2. 共享读锁(S锁)本身不会阻塞其他事务的读操作,仅会阻塞写操作。多个事务可以同时持有同一行的S锁,只有当事务需要将S锁升级为独占写锁(X锁)时,才会和其他持有S锁的事务产生冲突。

REPEATABLE READ级别结果不符合预期的原因

5个并发事务几乎同时启动,普通SELECT走MVCC快照读,都读取到了事务启动时value=1的快照版本,各自在内存中将值加1得到2,随后事务提交时执行更新操作:

  • 第一个拿到行写锁的事务,将值更新为2并提交释放锁
  • 剩下4个事务拿到写锁后,执行的是UPDATE test SET value=2 WHERE id=30,本质是覆盖更新,最终值还是保持为2,所以5次请求后结果仅为2,属于典型的丢失更新问题。

SERIALIZABLE级别出现死锁、部分失败的原因

InnoDB的SERIALIZABLE隔离级别会将所有普通SELECT隐式转换为SELECT ... LOCK IN SHARE MODE,自动加共享读锁:

  1. 多个并发事务同时拿到同一行的S锁,都可以正常读取到value=1的结果
  2. 所有事务后续都要执行更新操作,需要将自身持有的S锁升级为X锁,但此时其他事务都持有S锁,升级操作会互相阻塞,形成死锁
  3. MySQL死锁检测机制触发,随机回滚其中3个事务,仅剩下2个事务能依次执行成功:第一个将值更新为2,第二个读取到已提交的2后加1得到3,最终结果为3。

正确的实现方案

要实现每次调用都能正确累加1,避免丢失更新,可选以下三种方案:

  • 数据库原子更新:直接执行数据库层面的原子累加操作,无需修改隔离级别,性能最优,示例JPQL如下:
    @Modifying
    @Query("UPDATE test t SET t.value = t.value + 1 WHERE t.ipAddress = :ipAddress")
    void incrementValueByIp(@Param("ipAddress") String ipAddress);
    
  • 悲观锁实现:查询时显式加独占写锁,保证同一时间只有一个事务能读取该行数据,避免并发读冲突:
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<TestEntity> findByIpAddress(String ipAddress);
    
  • 乐观锁实现:实体类新增@Version字段,通过版本号校验更新冲突,冲突时自动重试即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 01:39:03