Spring Boot事务REPEATABLE_READ与SERIALIZABLE隔离级为何允许并发读?
核心认知偏差纠正
你对MySQL InnoDB隔离级别的实现逻辑存在两个关键误解:
- InnoDB的REPEATABLE READ隔离级别默认通过*多版本并发控制(MVCC)*实现,普通无锁SELECT走快照读逻辑,不会加任何读锁,多个并发事务可以同时读取同一行的快照版本,完全不会互相阻塞。只有显式声明加锁的SELECT(
SELECT ... LOCK IN SHARE MODE/SELECT ... FOR UPDATE)才会触发加锁逻辑。 - 共享读锁(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,自动加共享读锁:
- 多个并发事务同时拿到同一行的S锁,都可以正常读取到
value=1的结果 - 所有事务后续都要执行更新操作,需要将自身持有的S锁升级为X锁,但此时其他事务都持有S锁,升级操作会互相阻塞,形成死锁
- 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
相关产品推荐
相关产品推荐

