Mysql为何使用可重复读(Repeatable read)为默认隔离级别?

事务性质

我们都知道事务的几种性质 :原子性、一致性、隔离性、持久性(ACID)。

原子性(Atomicity):指事务是一个不可分割的最小工作单位,事务中的操作只有都发生和都不发生两种情况。

一致性(Consistency):事务必须使数据库从一个一致状态变换到另外一个一致状态,举一个栗子,李二给王五转账50元,其事务就是让李二账户上减去50元,王五账户上加上50元;

一致性是指其他事务看到的情况是要么李二还没有给王五转账的状态,要么王五已经成功接收到李二的50元转账。而对于李二少了50元,王五还没加上50元这个中间状态是不可见的。

隔离性(Isolation):一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(Durability):一个事务一旦提交成功,它对数据库中数据的改变将是永久性的,接下来的其他操作或故障不应对其有任何影响。

为了维持一致性和隔离性,一般使用加锁这种方式来处理,但是加锁相对带来的是并发处理能力的降低。

而数据库是个高并发的应用,因此对于加锁的处理是事务的精髓。

封锁协议(Locking Protocol)

MySQL的锁系统:shared lock 和 exclusive lock  即共享锁和排他锁,也叫读锁(S)和写锁(X),共享锁和排他锁都属于悲观锁。排他锁又可以可以分为行锁和表锁。

封锁协议:在使用X锁或S锁对数据加锁时,约定的一些规则。例如何时申请X或S锁,持续时间,何时释放锁等。

一级、二级、三级封锁协议

对封锁方式规定不同的规则,就形成了各种不同的封锁协议。不同的封锁协议,为并发操作的正确性提供不同程度的保证。

一级封锁协议

一级封锁协议:事务T在修改数据R之前必须先对其加X锁(排他锁),直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。

一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。

在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。

二级封锁协议

二级封锁协议:在一级封锁协议加上事务T在读取数据R之前必须先对其加S锁(共享锁),读完后释放S锁。事务的加锁和解锁严格分为两个阶段,第一阶段加锁,第二阶段解锁。

加锁阶段

在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。

解锁阶段

当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后释放S锁,所以它不能保证可重复读。

二级封锁的目的是保证并发调度的正确性。就是说,如果事务满足两段锁协议,那么事务的并发调度策略是串行性的。保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)

三级封锁协议

三级封锁协议:在一级封锁协议加上事务T在读取数据R之前必须先对其加S锁(共享锁),直到事务结束才释放。

三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。上述三级协议的主要区别在于什么操作需要申请封锁,以及何时释放。

执行了封锁协议之后,就可以克服数据库操作中的数据不一致所引起的问题。

事务四种隔离级别

在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。分别是:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable-read)、序列化(serializable)

读未提交

你在修改表中数据的时候,你可以看到另一个人没有提交的数据。这是一种安全级别最低的隔离级别,目前这种级别只是理论存在,因为目前基本没有数据库采用这种隔离方式,这种隔离级别会产生的问题就是dirty read(脏读)、不可重复读、幻读。

读已提交(rc

很好理解,假如此时,你和另外一个人同时操作同一张表,有一条数据是id为1 姓名为小明的数据,此时另外一个人对这个数据修改为id为2 姓名为小红(没有提交前),你再次查询得到的数据一直都是1 小明,而当这个人修改完后输入commit提交后,此时你在查询时,数据则会变为2 小红,这就是读已提交,意思其实和字面意思一样,只能读取当已提交的数据。这个隔离级别解决了脏读的问题,但是缺点就是不可重复读。

可重复读(rr)

这是mysql的默认隔离级别,与第二种不同的是,此时如果和你操作一张表的另一个人commit后,你select查询后依旧是1 小明,只有当你自己commit之后,当前事务完全结束后,你才可以看到修改后的值,这种隔离级别解决了不可重复读的问题,但是出现了一种新的问题,就是幻读,什么是幻读呢?

举个例子,当你查询某条数据时,可能显示的是1 小明,但是此时可能有个人正好修改好了这条数据,当你提交后再次查询时,你会发现变成了2 小红,这个时候你可能会觉得不对啊,刚才我刚查询的时候明明不是2 小红啊,明明是1小明啊,是我产生幻觉了吗?这就是幻读。

序列化

最安全的默认隔离级别,但是并不支持并发,相当于单线程,不允许并发操作数据库,事务开启后,只允许一个人进行操作,直到提交后下一个人才可以进入。

为什么是RR

一般的DBMS系统,默认都会使用读提交(Read-Comitted,RC)作为默认隔离级别,如Oracle、SQL Server等,而MySQL却使用可重复读(Read-Repeatable,RR)。要知道,越高的隔离级别,能解决的数据一致性问题越多,理论上性能的损耗更大,且并发性越低。

隔离级别依次为:

SERIALIZABLE > RR > RC > RU

可重复读(Repeated Read):可重复读。基于锁机制并发控制的DBMS需要对选定对象的读锁(read locks)和写锁(write locks)一直保持到事务结束,但不要求“范围锁(range-locks)”,因此可能会发生“幻影读(phantom reads)” 在该事务级别下,保证同一个事务从开始到结束获取到的数据一致。是Mysql的默认事务级别。

下面我们先来思考2个问题

在读已提交(Read Commited)级别下,出现不可重复读问题怎么办?需要解决么?

不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle ,SqlServer 默认隔离级别就是RC,我们也没有更改过它的默认隔离级别。

在Oracle,SqlServer中都是选择读已提交(Read Commited)作为默认的隔离级别,为什么Mysql不选择读已提交(Read Commited)作为默认隔离级别,而选择可重复读(Repeatable Read)作为默认的隔离级别呢?

历史原因,早阶段Mysql(5.1版本之前)的Binlog类型Statement是默认格式,即依次记录系统接受的SQL请求;5.1及以后,MySQL提供了Row,Mixed,statement  3种Binlog格式, 当binlog为statement格式,使用RC隔离级别时,会出现bug。

因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!

Binlog简介

Mysql binlog是二进制日志文件,用于记录mysql的数据更新或者潜在更新(比如DELETE语句执行删除而实际并没有符合条件的数据),在mysql主从复制中就是依靠的binlog。可以通过语句“show binlog events in 'binlogfile'”来查看binlog的具体事件类型。binlog记录的所有操作实际上都有对应的事件类型的。

MySQL binlog的三种工作模式:row、statement、mixed。

row:日志中会记录每一行数据被修改的情况,然后在slave端对相同的数据进行修改。优点:能清楚的记录每一行数据修改的细节 缺点:数据量太大。

statement:每一条被修改数据的sql都会记录到master的bin-log中,slave在复制的时候sql进程会解析成和原来master端执行过的相同的sql再次执行。在主从同步中一般是不建议用statement模式的,因为会有些语句不支持,比如语句中包含UUID函数,以及LOAD DATA IN FILE语句等。

优点:解决了 Row level下的缺点,不需要记录每一行的数据变化,减少bin-log日志量,节约磁盘IO,提高新能 缺点:容易出现主从复制不一致。

mixed:结合了Row level和Statement level的优点,同时binlog结构也更复杂。我们可以简单理解为binlog是一个记录数据库更改的文件,主从复制时需要此文件。

主从不一致实操

将binlog修改为statement格式,且隔离级别为读已提交(Read Commited)时,有什么bug呢?

mysql> select * from test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | NULL | NULL |
|  2 | NULL | NULL |
|  3 | NULL | NULL |
|  4 | NULL | NULL |
|  5 | NULL | NULL |
|  6 | NULL | NULL |
+----+------+------+
6 rows in set (0.00 sec)
session1session2
mysql> set tx_isolation = 'read-committed';
Query OK, 0 rows affected, 1 warning (0.00 sec)mysql> set tx_isolation = 'read-committed';
Query OK, 0 rows affected, 1 warning (0.00 sec)
begin; Query OK, 0 rows affected (0.00 sec)begin; Query OK, 0 rows affected (0.00 sec)
delete from test where 1=1;
Query OK, 6 rows affected (0.00 sec)
insert into test values (null,'name',100);
Query OK, 1 row affected (0.00 sec)
commit;
Query OK, 0 rows affected (0.01 sec)
commit;
Query OK, 0 rows affected (0.01 sec)

Master此时输出

select * from test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  7 | name |  100 |
+----+------+------+
1 row in set (0.00 sec)

但是,你在此时在从(slave)上执行该语句,得出输出

mysql> select * from test;
Empty set (0.00 sec)

在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,是基于事务记录,在事务未提交前,二进制日志先缓存,提交后再写入记录的,因此顺序为先插后删!slave同步的是binglog,因此从机执行的顺序和主机不一致!slave在插入后删除了所有数据。

解决方案有两种!
(1)隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住!

(2)将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。

因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!

UR和Serializable

项目中不太使用读未提交(Read UnCommitted)和串行化(Serializable)两个隔离级别,原因:

读未提交(Read UnCommitted)

允许脏读,也就是可能读取到其他会话中未提交事务修改的数据 一个事务读到另一个事务未提交读数据

串行化(Serializable)

使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。一般是使用mysql自带分布式事务功能时才使用该隔离级别。

参考文献:https://blog.51cto.com/u_1548...


杨帆
28 声望3 粉丝