查看原文
其他

阿里二面:为什么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



硬核面试题推荐            




硬核文章推荐            




硬核电子书            

👍尼恩Java面试宝典》(极致经典,不断升级)全网下载超过300万次

👍尼恩Java高并发三部曲:全网下载超过200万次

👍《顶级3高架构行业案例 + 尼恩架构笔记 》N 篇+,不断添加

👍100份简历模板

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存