相信很多读者对MySQL的事务隔离级别都不陌生。网上有很多相关的文章。很多人都熟悉各种隔离级别和一些可以通过不同级别解决的阅读现象。如果你对这部分知识不了解,可以看看我几年前写的那篇文章:数据库读现象分析什么是脏读、幻读、不可重复读以及它们的背景。深入分析事务的隔离级别介绍了数据库的隔离级别和解决的读现象问题。我们知道ANSI/ISOSQL定义了四种标准的隔离级别,从高到低分别是:Serializable、RepeatableReads、ReadCommitted、ReadUncommitted)。在RU隔离级别,可能会出现脏读、幻读、不可重复读等问题。RC隔离级别下解决了脏读问题,同时存在幻读和不可重复读问题。在RR隔离级别下,解决了脏读和不可重复读的问题,存在幻读问题。在Serializable隔离级别下,解决了脏读、幻读、不可重复读等问题。这四种隔离级别是由ANSI/ISOSQL定义的标准定义的,我们常用的MySQL都支持这四种隔离级别。但是Oracle数据库只支持Serializable和ReadCommitted。不过很多人可能不知道,Oracle默认的隔离级别是RC,而MySQL默认的隔离级别是RR。那么,你知道为什么Oracle选择RC作为默认级别,而MySQL选择RR作为默认隔离级别吗?这是我在之前的面试中问候选人的一个问题。很多人认为这个问题没有意义。这不是逼我们背八股文吗?其实不然,如果你能耐心看完这篇文章,你就会发现我的良苦用心。Oracle的隔离级别我们前面说过,Oracle只支持ANSI/ISOSQL定义的Serializable和ReadCommitted。实际上,根据Oracle官方文档给出的介绍,Oracle支持三种隔离级别:Oracle支持ReadCommitted、Serializable和Read-Only。Read-Only只读隔离级别类似于serializable隔离级别,但是只读事务不允许在事务中修改数据,除非用户是SYS。Oracle的三个隔离级别中,Serializable和Read-Only显然不适合作为默认的隔离级别,ReadCommitted是唯一剩下的选项。与Oracle相比,MySQL的隔离级别对于MySQL默认的隔离级别有更大范围的选择。首先我们把Serializable和ReadUncommitted排除在四个隔离级别之外,主要是因为这两个级别一个太高,一个太低。太高会影响并发,太低会出现脏读。那么,如何选择剩下的RR和RC呢?这件事很久很久以前就开始了。在MySQL的设计中,它的定位是提供一个稳定的关系型数据库。为了解决MySQL单点故障带来的问题,MySQL采用了主从复制机制。所谓主从复制,其实就是通过搭建MySQL集群对外提供服务。集群中的机器分为主服务器(Master)和从服务器(Slave)。主服务器提供写服务,从服务器提供读服务。为了保证主从服务器之间数据的一致性,需要进行数据同步。大致的同步流程如下,这里不再详细介绍。MySQL主从复制过程中,通过binlog进行数据同步。简单理解就是主服务器在binlog中记录数据变化,然后将binlog同步传输给从服务器,从服务器接收bin日志。之后,将其中的数据恢复到自己的数据库存储中。那么,binlog中记录了什么?格式是什么?MySQL的binlog主要支持三种格式,分别是statement、row、mixed。MySQL从5.1.5版本开始支持row,5.1.8版本开始支持mixed。statement和row最大的不同在于,当??binlog的格式为statement时,binlog中记录的是sql语句的原文(这句话很重要!!!后面会用到)。这里不详细讨论这些格式之间的区别。之所以支持行格式,主要是因为语句格式存在很多问题,最明显的就是可能会导致主从数据库数据不一致。详细介绍可以参考极客时间丁奇的分享《MySQL实战45讲》。那么,这个主从同步和我们要讲的binlog中的隔离级别是什么关系呢?这很重要,而且非常重要。因为早期MySQL只有statement的binlog格式,此时如果使用已提交读(ReadCommitted)和未提交读(ReadUncommitted)这两个隔离级别,就会出现问题。比如在MySQL官网上,曾经有人向官方提过一个相关的bug。该bug的重现过程如下:数据库表t1有如下两条记录:CREATETABLEt1(aint(11)DEFAULTNULL,bint(11)DEFAULTNULL,KEYa(a))ENGINE=InnoDBDEFAULTCHARSET=latin1;insertintot1values(10,2),(20,1);然后开始执行两个事务的写操作:上面两个事务执行完后,数据库中的记录会变成(11,2)和(20,2),这个帖子在大家可以看懂的数据变化主数据库。因为事务的隔离级别是readcommitted,事务1在更新时,只会对行b=2加一个行级锁,不会影响事务2对行b=1的写操作。上面两个事务执行后,binlog中会记录两条记录,因为先提交了事务2,所以UPDATEt1SETb=2whereb=1;会先记录,然后UPDATEt1SETa=11whereb=2;(再次提醒:statement格式的binlog记录的是sql语句的原文)binlog同步到备库后,回放sql语句时,UPDATEt1SETb=2whereb=1;会先执行,然后执行UPDATEt1SETa=11whereb=2;。这时数据库中的数据就会变成(11,2)和(11,2)。这样就导致主备库数据不一致!!!为了避免此类问题的发生。MySQL将数据库的默认隔离级别设置为RepetableRead。那么,RepetableRead的隔离级别是如何解决这个问题的呢?那是因为RepetableRead的隔离级别在更新数据的时候不仅会在更新的行上加行级锁,还会增加GAP锁。在上面的例子中,当事务2执行时,由于事务1加了GAP锁,事务执行会卡住,事务1需要提交或者回滚才能继续执行。(关于GAP锁,我会在后面的文章中单独介绍)。MySQL除了设置默认隔离级别外,在使用statement格式的binlog时,也禁止使用READCOMMITTED作为事务隔离级别。一旦用户主动修改隔离级别并尝试更新,就会报错:ERROR1598(HY000):Binaryloggingnotpossible。Message:Transactionlevel'READ-COMMITTED'inInnoDBisnotsafeforbinlogmode'STATEMENT'总结那么,现在我们知道为什么MySQL选择RR作为默认的数据库隔离级别,其实就是为了兼容statement格式的历史binlog。所以,这篇文章大约是MySQL隔离级别知识的不到1/5。通过本文,您可能还有以下疑问:1.行格式和语句有什么区别?使用row时,可以使用RR吗?2、文中提到的RC的GAP锁是什么?3.RR和RC有什么区别?RC是如何解决不可重复读问题的?4、既然MySQL数据库默认选择的是RR,为什么像阿里这样的互联网大公司要把默认的隔离级别改成RC呢?关于以上几个问题,你知道答案吗,或者你更感兴趣的是哪一个呢?欢迎留言!我会挑选大家比较感兴趣的话题,在后续的文章中继续深入介绍。
