问题
为什么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格式的优势
- 不依赖隔离级别:记录实际数据变化,主从一定一致
- 更精确:记录实际影响的行
- 更安全:避免函数(如NOW()、RAND())导致的不一致
为什么不改默认隔离级别为RC?
- 向后兼容性:大量已有系统依赖RR级别的行为
- Statement格式仍在使用:很多老系统使用Statement或Mixed格式
- 保守策略:改变默认隔离级别风险大,影响面广
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复制安全性 | 安全 | 安全 |
| 并发性能 | 较低(间隙锁多) | 较高(只有记录锁) |
| 锁竞争 | 多(间隙锁) | 少 |
| 死锁概率 | 较高 | 较低 |
| 一致性读 | 强(事务级快照) | 弱(语句级快照) |
面试要点总结
- 历史原因:MySQL早期只支持Statement格式Binlog,RC级别会导致主从不一致
- RR + Next-Key Lock:通过锁机制保证事务执行顺序,确保Binlog顺序与执行效果一致
- Row格式Binlog出现后,理论上可以改用RC,但为了向后兼容,保留了RR默认值
- 现代实践:Row格式Binlog + RC隔离级别是更好的选择
- 大厂改用RC:减少锁竞争、提高并发、降低死锁
- 理解这个问题需要结合事务隔离级别、锁机制、Binlog复制三个维度