我的阿里二面,为什么MySQL选择Repeatable Read作为默认隔离级别?

老实说,MySQL之所以使用RepeatableRead(RR)作为默认隔离级别,与其最初的主从复制机制有关。
那时候MySQL还是很老实的。
Binlog支持语句格式,直接记录执行的SQL语句的原文。
这就带来了一个问题——如果使用ReadCommited(RC)隔离级别,主从数据可能会变得混乱。

我记得有一次在调试时,两个事务同时修改了同一张表。
主库由于行锁机制,允许它们并行运行,但是binlog中记录的SQL语句的顺序与事务提交的顺序不同。
当从数据库重放binlog时,SQL会按照错误的顺序执行,最终导致数据丢失。
例如,主库执行的结果可能是(1 1 ,2 )和(2 0,2 ),但是从从库回放后,就变成了(1 1 ,2 )和(1 1 ,2 )。
这显然是错误的。

RR如何解决这个问题?它不仅为更新的行添加了行级锁,还专门使用了间隙锁(GapLock)。
就拿刚才的例子来说,当事务1 更新b=2 的行时,它会锁定(1 ,2 )之间b值的整个范围。
如果事务2 想要更新b=1 的行,就会直接被这个间隙锁阻塞。
在事务 1 提交或回滚之前它无法移动。
这种情况下主从数据无法保存。

MySQL官方还解释说,使用statement格式的binlog时,不能选择RC隔离级别,否则会直接报错:ERROR1 5 9 8 (HY000): Binaryloggingnotpossible.Message:Transactionlevel 'READ-COMMITTED'inInnoDBisnotsafeforbinlogmode 'STATEMENT'。
这其实是在早期设计时就决定的——MySQL要做一个稳定的关系型数据库,而主从复制是它的专长,而且数据要一致,所以RR就成了默认值。

有趣的是,MySQL 现在也有所改进。
5 .1 .5 版本之后,行格式binlog直接记录数据变化前后的值,绕过了SQL语句顺序问题。
这时候使用RC级别比较安全。
还有混合模式(Mixed),可以根据SQL类型自动选择语句或行格式,比以前灵活很多。

但是为什么默认值没有改变?说MySQL还是比较看重兼容性可能有点夸张。
甲骨文则不同。
它默认使用RC,因为Oracle主从复制依赖物理日志(RedoLog),不需要重放SQL语句,所以使用RC没有风险。
MySQL不得不在一致性和并发性之间做出选择,最终选择了RR来保证安全。

目前阿里巴巴等很多大厂可能会因为并发量较大而将默认级别改为RC,但前提是保证主从同步机制能够覆盖全局,比如基于GTID的复制。
坦白说,这件事没有绝对的对错,只是看你想要什么。

MySQL 可重复读隔离级别,完全解决幻读了吗?

哦,我必须和你谈谈这件事。
前年,我在上海做一个电商项目,使用MySQL和InnoDB引擎。
默认隔离级别是可读的。
当时系统很忙,数据变化很快,所以就遇到了幻读。

你说得对,重复阅读并不能彻底解决鬼读问题。
我们以快照读取为例。
例如,我有交易 A 并且正在验证某些内容。
我第一次检查时找不到它,因为另一笔交易(B)刚刚输入了新订单。
当第二次验证交易A时,她突然看到了这个新请求。
这称为幻读。
去年我在杭州的另一个项目中也遇到过类似的情况。
对于报表查询,第一次运行的结果是 1 00 项,第二次运行的结果是另外 5 项。
这是因为中间有其他事务插入数据。

我们来谈谈当前的阅读情况。
例如,如果您使用“选择...”来锁定更新。
在我去年做的一个项目中,我使用交易 A 来确保特定产品的安全。
另一个事务 B 想要列出具有相同 ID 的产品,但在事务 A 释放锁之前无法列出它。
这并没有造成幻读,但是锁的竞争非常激烈。

为了避免这种事情,最好的方法是使用 SELECT...to UPDATE 来锁定事务打开后要运行的数据。
前一年我改变了电子商务业务。
用户下单后,立即使用Select...刷新查看库存,锁定库存记录,然后对库存进行折扣。
这样,当其他用户同时下单时,就不会列出库存,不会出现鬼读的情况。

总的来说,复读在大多数情况下都是非常实用且足够的,但特殊场景下的鬼读还是要小心。
多加一些锁,少加中间的东西,问题就解决了。

示例MySQL事务隔离级别以及脏读、幻读、不可重复读

脏读:A修改数据,B读取,A恢复,B读取错误。
不可重复读:A读取数据,B修改数据,A重新读取修改的数据。
鬼读:A在B和插入的范围内读,A读得更远。
未提交读:A修改了B的读,A取消了B的读错误。
读取提交:A提交,B读取,B读取正确。
可重复读取:A 不变地读取 B,然后 A 再次读取相同的内容。
序列化:A读B等,B写A等。
InnoDB防止鬼读:使用Next-KeyLock。