阿里二面:为什么MySQL默认Repeatable Read隔离级别,大厂要改成RC?
前段时间,一位小伙伴面试了阿里 ,遇到了一个 MySQL 日志的面试题:
为什么MySQL默认的Repeatable Read隔离级别,被改成了RC?
这个问题,背后的原理比较复杂,而且又很重要。
现在把这个 题目,以及参考答案,收入咱们的 《尼恩Java面试宝典》,供后面的小伙伴参考,前车之鉴啊
ISO SQL定义的标准隔离级别有四种
我们知道,我们可以通过这个命令查看数据库当前的隔离级别,MySQL 默认隔离级别是RR.
select @@tx_isolation;
ANSI/ISO SQL定义的标准隔离级别有四种,从高到底依次为:
可序列化(Serializable)、可重复读(Repeatable Reads)、提交读(Read Committed)、未提交读(Read Uncommitted)。
这四个隔离级别可以解决脏读、不可重复读、幻象读这三类问题。总结如下
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 RU | 允许 | 允许 | 允许 |
读已提交 RC | 不允许 | 允许 | 允许 |
可重复读 RR | 不允许 | 不允许 | 允许 |
串行化 | 不允许 | 不允许 | 不允许 |
RU隔离级别下,可能发生脏读、幻读、不可重复读等问题。
未提交读的数据库锁情况(实现原理)
事务在读数据的时候并未对数据加锁。
事务在修改数据的时候只对数据增加行级共享锁。
RC隔离级别下,解决了脏读的问题,存在幻读、不可重复读的问题。
提交读的数据库锁情况
事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
RR隔离级别下,解决了脏读、不可重复读的问题,存在幻读的问题。
可重复读的数据库锁情况
事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加 行级共享锁,直到事务结束才释放;
事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
Serializable隔离级别下,解决了脏读、幻读、不可重复读的问题。
可序列化的数据库锁情况
事务在读取数据时,必须先对其加 表级共享锁 ,直到事务结束才释放;
事务在更新数据时,必须先对其加 表级排他锁 ,直到事务结束才释放。
虽然可序列化解决了脏读、不可重复读、幻读等读现象。但是序列化事务会产生以下效果:
1.无法读取其它事务已修改但未提交的记录。 2.在当前事务完成之前,其它事务不能修改目前事务已读取的记录。 3.在当前事务完成之前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。
这四种隔离级别是ANSI/ISO SQL定义的标准定义的,我们比较常用的MySQL对这四种隔离级别是都支持的。
其中隔离级别由低到高是:读未提交 < 读已提交 < 可重复读 < 串行化。隔离级别越高,越能够保证数据的完整性和一致性,但是对并发的性能影响越大。
注意,Mysql通过间隙锁解决了RR级别下的幻读问题,所以,Mysql的RR级别,也解决了脏读、幻读、不可重复读的问题。
大多数数据库的默认级别是读已提交(Read committed)
,比如 Sql Server、Oracle ,
但是 MySQL 的默认隔离级别是 可重复读(repeatable-read)
。
大家经常说:Oracle默认的隔离级别是 RC,而MySQL默认的隔离级别是 RR。
那么,为什么 MySQL 默认隔离级别是RR,为什么阿里等大厂会改成RC?
根本的原因:提升MYSQL 吞吐量、并发量。允许很短的时间内数据不可重读、幻读,或者在业务维度去规避。
互联网公司和传统企业最大的区别是什么?高并发!互联网业务的并发度比传统企业要高处很多。
这点,从一个简单的数据可以看出: 2020年双十一当天,订单创建峰值达到 58.3 万笔/秒。
MySQL的RR”可重复读“这种隔离级别,带来了很大的性能损耗。
无论是超高并发读的场景,还是超高并发写的场景,带来了一些的性能损耗。
RR在高并发写场景的性能损耗
在 MySQL 中,有三种类型的锁,分别是Record Lock、Gap Lock和 Next-Key Lock。
Record Lock表示记录锁,锁的是索引记录。
Gap Lock是间隙锁,锁的是索引记录之间的间隙。
Next-Key Lock是Record Lock和Gap Lock的组合,同时锁索引记录和间隙。他的范围是左开右闭的。
在 RC 中,只会对索引增加Record Lock,不会添加Gap Lock和Next-Key Lock。
在 RR 中,为了解决幻读的问题,在支持Record Lock的同时,还支持Gap Lock和Next-Key Lock;
间隙锁的触发条件:
事务隔离级别为RR。因为间隙锁只有在事务隔离级别RR中才会产生,隔离级别级别是RC的话,间隙锁将会失效 显式加锁。比如使用了类似于select…for update这样的加锁语句 查询条件必须有索引:
(1)若查询条件走唯一索引:只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁;如果查询单条存在的记录,不会产生间隙锁
(2)若查询条件走普通索引:不管是锁住单条,还是多条记录,都会产生间隙锁
间隙锁属于排它锁,它会把查询sql中最靠近检索条件的左右两边的数据间隙锁住,防止其它事务在这个间隙内插入、修改、删除数据,从而保证该事务内,任何时候以相同检索条件读取的数据都是相同,也即保证可重复读。
在高并发下,由于间隙被锁住,导致需要往间隙内插入、删除、修改数据的并发线程必须等待,会带来一定性能问题,并且最终锁影响的范围可能远远超过我们想要操作的数据。
间隙锁主要是解决RR级别的幻读问题而来。但在现实中,一个事务里重复同一条sql再次查询的场景极低,且出于性能的考虑,一般也会尽量避免在同一事务内对同一数据进行多次查询。
在高并发、分布式RPC应用场景,通过加Redis 分布式Cache的方式,其实也就避免了多次查询。没有多次查询,或者避免多次查询,也就无所谓幻读。
如果实际业务场景中,如果无需锁住数据间隙,建议关闭间隙锁,或者将MySQL隔离级别由RR改为RC,否则会带来无谓的性能开销,甚至会引发死锁,影响业务运行。
RR在高并发读场景的性能损耗
为了提升性能,RR与RC下的普通读都是快照读,这里提到的普通读, 是指除了如下2种之外的select都是普通读
select * from table where ... for update;
select * from table where ... lock in share mode;
所以普通读的范围,是非常大的。
快照读,又称为一致性读。快照即当前行数据之前的历史版本。快照读就是使用快照信息显示基于某个时间点的查询结果,而不考虑与此同时运行的其他事务所执行的更改。
在MySQL 中,只有READ COMMITTED 和 REPEATABLE READ这两种事务隔离级别才会使用一致性读。
RR与RC下的普通读都是快照读,但两者的快照读有所不同:
RC下,事务内每次都是读最新版本的快照数据 RR下,事务内每次都是读同一版本的快照数据(即首次read时的版本)
RR下,事务会以第一次普通读时快照数据为准,该事务后续其他的普通读都是读的该份快照数据,也即事务内是同一份快照读。这就意味着,Mysql为了维护同一版本的快照数据,需要额外的资源耗损,含计算耗损、内存耗损。
但在现实中,一个事务里重复同一条sql再次查询的场景极低,且出于性能的考虑,一般也会尽量避免在同一事务内对同一数据进行多次查询。因此,RR下所谓的事务内同一份快照读意义并不大。
总之:
MySQL的RR”可重复读“这种隔离级别,带来了很大的性能损耗。
无论是超高并发读的场景,还是超高并发写的场景,带来了一些的性能损耗。
MySQL 默认隔离级别是RR,为什么阿里等大厂会改成RC,主要是出于性能考虑。
另外,通过程序手段,去规避幻读、可重复读的问题。
End
此真题面试题,收录于《尼恩Java面试宝典》V34
硬核面试题推荐
如何写简历:2023春招,让简历 人见人爱 的8大绝招 | 附100模板 场景题:假设10W人突访,系统能不crash 吗? 每天100w次登陆请求, 8G 内存该如何设置JVM参数?来看看年薪100W的架构师,是怎么配置的
硬核文章推荐
硬核电子书
👍《尼恩Java面试宝典》(极致经典,不断升级)全网下载超过300万次
👍尼恩Java高并发三部曲:全网下载超过200万次
👍《Java高并发核心编程-卷1(加强版)》,不断升级
👍《Java高并发核心编程-卷2(加强版)》,不断升级
👍《Java高并发核心编程-卷3(加强版)》,不断升级
👍《顶级3高架构行业案例 + 尼恩架构笔记 》N 篇+,不断添加
👍100份简历模板