一、本质区别:一句话
- RC(Read Committed) :每条 SELECT 生成新 read view,每次读都看到最新已提交数据。
- RR(Repeatable Read) :事务开始生成一次 read view,整个事务看到同一份快照。
RR 解决的是"读-读一致性"------同事务内多次 SELECT 同一行,值不变。
但乐观锁要解决的是"读-写冲突"------你基于读到的旧值算出新值,UPDATE 时会覆盖别人已经提交的修改。
差别只在读 ------读一致性不同。写的行为两者一样。
这是两个完全不同的问题 。RR 把第一个问题解决了,但对第二个问题毫无帮助------而且因为快照读让你看不到别人的修改,反而让"基于旧值的覆盖写"更隐蔽。
二、各级别解决什么问题
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ_UNCOMMITTED | × | × | × |
| READ_COMMITTED(RC) | √ | × | × |
| REPEATABLE_READ(SQL 标准) | √ | √ | × |
| REPEATABLE_READ(InnoDB) | √ | √ | √(next-key lock 额外解决) |
| SERIALIZABLE | √ | √ | √ |
关键:RC 只解决脏读,不可重复读要 RR 才解决。InnoDB 在 RR 上"超标准"是解决幻读,不是不可重复读。
三、Spring 隔离级别和数据库的关系
Spring 不实现 隔离级别,只是把设置委托给数据库------事务开启时调 Connection.setTransactionIsolation(level),相当于执行 SET SESSION TRANSACTION ISOLATION LEVEL xxx,只对当前会话生效。事务结束后 Spring 自动恢复原级别,不污染连接池。
所以 :MySQL 全局设 RC,Spring 方法上 @Transactional(isolation = RR) 完全可行、无副作用------但生产里不建议混用(间隙锁死锁风险、调试复杂、收益不大)。
四、读一致性 vs 写冲突------两个维度,不可替代
这是几轮讨论的核心。隔离级别和乐观锁解决的是两个完全不同的问题:
| 问题 | 谁来解决 | RC 够吗 | RR 够吗 |
|---|---|---|---|
| 同事务内多次读同一行值一致(读一致性) | 隔离级别 | × | √ |
| 基于读到的旧值算新值,UPDATE 时不覆盖别人的修改(写冲突) | 乐观锁/原子 SQL | × | × |
结论 :RR 解决了"读一致性",但没解决"写冲突" ------所以 RC 和 RR 下"读-判-写"模式都需要乐观锁兜底。
RR 下乐观锁的代码、SQL、行为和 RC 下完全一样 ------因为乐观锁的"判断"是数据库在 UPDATE 执行时根据当前行的真实 version 做的,跟事务隔离级别无关。
五、RC 和 RR 下乐观锁的代码完全一样
bash
UPDATE product_stock
SET stock = #{newStock}, version = version + 1
WHERE id = #{id} AND version = #{oldVersion}
不管隔离级别是 RC 还是 RR,这条 SQL 都能在写入时校验"我读到的版本是否还有效",rows=0 表示有冲突。乐观锁跟隔离级别无关。
六、RC 和 RR 下都会超卖------具体场景
同样并发扣库存,RC 和 RR 结果都一样:超卖。原因相同------基于读到的值算新值,UPDATE 直接覆盖写入,数据库不校验"读到这里"。
RR 反而更隐蔽:快照读让事务感知不到别人的修改,更"自信"地覆盖写入,问题更难排查。
七、实战方案对比
| 业务模式 | 推荐方案 | 适用场景 |
|---|---|---|
| 纯数值加减(库存、余额) | SQL 原子操作 SET stock = stock - ? WHERE stock >= ? |
不需要先 SELECT |
| 需要拿旧值做校验(订单状态机) | 乐观锁 WHERE version = ? |
冲突不频繁 |
| 需要拿旧值 + 冲突极频繁 | 悲观锁 SELECT FOR UPDATE |
热点行 |
| 跨服务/跨库一致性 | 事务消息 + 本地消息表 / Seata | 最终一致 |
八、大厂主流选型
阿里、美团这类大厂的订单/支付系统:
- 全局隔离级别 = RC(性能优先,没有间隙锁死锁)。
- 关键表带 version 字段。
- 纯数值扣减 → SQL 原子操作。
- 状态机流转 → 乐观锁
WHERE status=? AND version=?。 - 热点行(秒杀) → Redis 预扣 + 异步落库,绕开数据库行锁。
- 跨服务一致性 → RocketMQ 事务消息 + 本地消息表,最终一致。
九、一句话核心结论
隔离级别管"读",乐观锁管"写",两个维度不可替代。
- 选 RC 还是 RR:影响"读一致性"和"性能/死锁风险",是业务读场景的取舍。
- 加不加乐观锁:影响"写冲突"是否能兜底,是写场景的必备。
无论 RC 还是 RR,只要代码模式是"先读再基于读到的值算新值并写入",都必须加乐观锁或用原子 SQL。隔离级别选什么,都救不了"基于旧值的覆盖写"。
实战选型一句话
- RC + 乐观锁:大厂主流,性能优先,乐观锁兜底写冲突。
- RR + 乐观锁:金融/对账场景,写一致性和读一致性都要,乐观锁还是要加。
- RR 不加乐观锁 :仅当你的 UPDATE 是"纯数值原子操作"(SET stock = stock - ?)或"无依赖的状态覆盖"(SET status='CANCELLED' WHERE id=?)才安全。任何"先 SELECT 再基于读到的值算新值写入"的模式,RR 下都必须加乐观锁或用原子 SQL。
这份总结覆盖前面 4 轮讨论的所有要点。核心就一句话------RC/RR 决定你看到的"读世界",但写入时还是要靠 version 校验,让数据库在写入那一刻确认你看到的世界还成立。