问题

为什么MySQL默认使用RR隔离级别?

答案

核心原因

MySQL InnoDB默认使用REPEATABLE READ(可重复读)隔离级别,主要是为了保证基于Binlog的主从复制数据一致性,这是一个历史遗留的设计决策。


1. 历史背景

MySQL早期的复制机制

在MySQL 5.0之前,主从复制只支持Statement-Based Replication(SBR,基于语句的复制)

  • 主库执行SQL语句
  • 将SQL语句写入Binlog
  • 从库读取Binlog并重放SQL语句
主库:UPDATE user SET balance = balance + 100 WHERE id > 10;
       ↓
Binlog:记录SQL语句本身
       ↓
从库:执行相同的SQL语句

问题:Statement复制在RC级别下可能导致主从不一致


2. Statement复制 + RC级别的问题

问题场景

主库执行
-- 主库(READ COMMITTED级别)

-- 事务A(trx_id=100)
START TRANSACTION;
INSERT INTO user (id, name) VALUES (5, 'Alice'); -- 未提交

-- 事务B(trx_id=101)
START TRANSACTION;
UPDATE user SET score = score + 10 WHERE id > 3;
-- 在RC级别下,UPDATE能看到id=4的记录,但看不到id=5(事务A未提交)
-- 假设更新了id=4,影响1行
COMMIT;

-- 事务A提交
COMMIT;
Binlog记录顺序
Binlog中的记录顺序(按提交顺序):
1. UPDATE user SET score = score + 10 WHERE id > 3;  (事务B)
2. INSERT INTO user (id, name) VALUES (5, 'Alice');  (事务A)
从库回放
-- 从库执行(按Binlog顺序)

-- 1. 先执行事务B的UPDATE
UPDATE user SET score = score + 10 WHERE id > 3;
-- 此时从库没有id=5的记录,只更新id=4

-- 2. 再执行事务A的INSERT
INSERT INTO user (id, name) VALUES (5, 'Alice');

问题:从库执行UPDATE时,id=5还不存在,因此UPDATE的结果与主库不一致。


主库 vs 从库结果对比
id=4的score id=5的score 说明
主库 +10 未更新 UPDATE时id=5未提交,不可见
从库 +10 未更新 UPDATE时id=5不存在

看起来一致?但如果SQL更复杂,可能导致不一致。


更严重的例子

-- 主库(RC级别)
-- 表初始:id=1(score=50), id=2(score=60)

-- 事务A
START TRANSACTION;
INSERT INTO user VALUES (3, 'Tom', 70);

-- 事务B
START TRANSACTION;
UPDATE user SET score = score + 10 WHERE score >= 60;
-- RC级别:只能看到id=2,更新1行
COMMIT;

-- 事务A提交
COMMIT;

-- Binlog顺序:
-- 1. UPDATE user SET score = score + 10 WHERE score >= 60;
-- 2. INSERT INTO user VALUES (3, 'Tom', 70);

-- 从库执行:
-- 1. 先INSERT id=3, score=70
-- 2. 再UPDATE score >= 60,会更新id=2和id=3,影响2行!

-- 结果:主库id=3的score=70,从库id=3的score=80(不一致!)

3. RR级别如何解决这个问题

RR + Next-Key Lock

REPEATABLE READ级别下,InnoDB通过Next-Key Lock(记录锁+间隙锁)锁定查询范围:

-- 主库(RR级别)

-- 事务B
START TRANSACTION;
UPDATE user SET score = score + 10 WHERE score >= 60;
-- 加Next-Key Lock,锁定 score >= 60 的范围
-- 包括间隙锁,阻止其他事务在此范围内插入

-- 事务A尝试插入
INSERT INTO user VALUES (3, 'Tom', 70); -- score=70在锁定范围内
-- 被阻塞,等待事务B提交

-- 事务B提交
COMMIT; -- 释放锁

-- 事务A才能插入
INSERT INTO user VALUES (3, 'Tom', 70);
COMMIT;

关键点

  • 事务的提交顺序与执行顺序一致
  • Binlog中的语句顺序与实际执行效果一致
  • 从库回放时能保证相同的结果

4. Row格式Binlog的出现

MySQL 5.1引入Row-Based Replication(RBR)

Statement格式:记录SQL语句
UPDATE user SET score = score + 10 WHERE id > 3;

Row格式:记录每一行的实际变更
id=4: score 50 -> 60
id=5: score 70 -> 80

Row格式的优势

  1. 不依赖隔离级别:记录实际数据变化,主从一定一致
  2. 更精确:记录实际影响的行
  3. 更安全:避免函数(如NOW()、RAND())导致的不一致

为什么不改默认隔离级别为RC?

  1. 向后兼容性:大量已有系统依赖RR级别的行为
  2. Statement格式仍在使用:很多老系统使用Statement或Mixed格式
  3. 保守策略:改变默认隔离级别风险大,影响面广

5. 其他考量因素

1. 业务一致性需求

RR级别提供更强的一致性保证:

-- 统计报表场景
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 100条
-- ... 业务逻辑
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- RR:仍是100条
COMMIT;

2. 兼容性

  • 很多应用假设隔离级别是RR
  • 改变默认级别可能破坏现有应用

3. SQL标准

  • SQL标准定义了4个隔离级别
  • RR作为中间级别,平衡了一致性和性能

6. 现代MySQL的变化

主从复制推荐配置

# 推荐使用Row格式
binlog_format = ROW

# 可以改用RC隔离级别
transaction_isolation = READ-COMMITTED

大厂实践

  • 阿里、腾讯等大厂:改用RC级别 + Row格式Binlog
  • 原因
    • 减少间隙锁,提高并发
    • 降低死锁概率
    • Row格式保证主从一致

7. 总结对比

因素 RR级别 RC级别
Statement复制安全性 安全(有Next-Key Lock) 不安全(可能主从不一致)
Row复制安全性 安全 安全
并发性能 较低(间隙锁多) 较高(只有记录锁)
锁竞争 多(间隙锁)
死锁概率 较高 较低
一致性读 强(事务级快照) 弱(语句级快照)

面试要点总结

  1. 历史原因:MySQL早期只支持Statement格式Binlog,RC级别会导致主从不一致
  2. RR + Next-Key Lock:通过锁机制保证事务执行顺序,确保Binlog顺序与执行效果一致
  3. Row格式Binlog出现后,理论上可以改用RC,但为了向后兼容,保留了RR默认值
  4. 现代实践:Row格式Binlog + RC隔离级别是更好的选择
  5. 大厂改用RC:减少锁竞争、提高并发、降低死锁
  6. 理解这个问题需要结合事务隔离级别、锁机制、Binlog复制三个维度