关系型数据库的事务有四个特性,其中一个就是隔离性(关于事务的更多信息,可以参考之前的一篇文章:漫谈MySQL中的事务)。为了满足不同的应用场景需求,有不同的隔离级别可供选择,在SQL标准和MySQL数据库中,共定义了四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,隔离级别越低,系统的开销和并发通常就会更高,但同时也可能带来脏数据等问题。

今天我们就用实例来演示这四种隔离级别的具体情况,为了简便起见,我们创建一个结构非常简单的表,并插入一条数据以供测试:

CREATETABLE`t_test`(
`id`int(11)NOTNULLAUTO_INCREMENT,
`name`varchar(255)DEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

INSERTINTO`t_test`VALUES('1','John');

一、读未提交

读未提交是最低的事务隔离级别,事务中的修改,即使没有提交,在其它事务中也是可见的,有时候也称为脏读,在实际开发中使用得很少:

打开网易新闻 查看精彩图片

先设置事务的隔离级别,事务一更新表中的唯一记录,并且确认更新成功,但事务还没有提交,也就是没有COMMIT,然后,执行事务二的SQL,但发现它已经可以读取事务一更新后的数据了。以下是事务一的SQL语句:

#设置事务隔离级别为读未提交
SETGLOBALTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;
#开始事务
STARTTRANSACTION;
#更新表中id=1的数据
UPDATEt_testSETname='Tom'WHEREid=1;
#确认更新成功
SELECT*FROMt_testWHEREid=1;

接下来执行事务二中的SQL,发现它竟然读到了事务一中没有提交的更新:

SETGLOBALTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;
STARTTRANSACTION;
SELECT*FROMt_testWHEREid=1;

二、读已提交

读已提交可以避免脏读,但它有不可重复读的问题,就是两次读取的数据不一样:

打开网易新闻 查看精彩图片

事务一更新表中的数据,但还没有提交,这时候事务二去读取数据的话,结果就是更新前的数据,这样就防止了脏读,但如果事务一提交以后,事务二再次读到的就是更新以后的数据了,这样就出现了两次同样的查询,但结果却不一样的情况。

另外,为了演示方便,在SQL中加入了SLEEP函数,这样可以方便地让两个事务按照我们想要的顺序进行执行,关于SLEEP函数,在之前的文章也提到过,它能够让当前进程暂停执行一段时间,单位为秒。

事务一的SQL如下(在SQL中也加入了详细的注释):

SETGLOBALTRANSACTIONISOLATIONLEVELREADCOMMITTED;
STARTTRANSACTION;
#更新表中id=2的数据
UPDATEt_testSETname='Tom'WHEREid=1;
#确认上面的UPDATE语句执行成功
SELECT*FROMt_testWHEREid=1;
#让当前进程暂停5秒,让事务二开始执行,方便观察数据变化情况
SELECTSLEEP(5)FROMdual;
COMMIT;

事务二的SQL如下:

SETGLOBALTRANSACTIONISOLATIONLEVELREADCOMMITTED;
STARTTRANSACTION;
#在事务一更新数据后、事务提交前查询数据,发现还是更新前的数据
SELECT*FROMt_testWHEREid=1;
#暂停10秒,以便让事务一提交
SELECTSLEEP(10)FROMdual;
#再次查询id=1的数据,发现已经变成Tom了,也就是事务一更新后的数据
SELECT*FROMt_testWHEREid=1;

我们先执行事务一的SQL,紧接着再执行事务二的SQL,由于加入了SLEEP函数,可以很方便地观察到:事务二不会读到事务一未提交的数据,但是同样的SQL查询,却出现了不同的结果,也就是存在不可重复读的问题。

三、可重复读

上面提到,对于读已提交的隔离级别,它不能解决不可重复读的问题,而可重复读则可以搞定,它保证在一次事务中多次读取同样记录的结果是一致的,然而,它不能解决幻读的情况:当前事务在读取某个范围内的数据时,其它事务又在此范围内插入了新的记录,前一个事务再次读取时会出现幻行。

由于可重复读相对复杂一些,它也是MySQL默认的事务隔离级别,我们分成两部分来进行演示:它可解决不可重复读的问题;存在幻读的现象。

首先来看可重复读的情况,也就是多次读取结果都一致:

打开网易新闻 查看精彩图片

可以看到,事务二读取到的数据总是更新前的数据,也就是多次读取的结果是一样的。事务一的SQL如下:

SETGLOBALTRANSACTIONISOLATIONLEVELREPEATABLEREAD;
STARTTRANSACTION;
UPDATEt_testSETname='Tom'WHEREid=1;
SELECT*FROMt_testWHEREid=1;
SELECTSLEEP(5)FROMdual;
COMMIT;

事务二的SQL:

SETGLOBALTRANSACTIONISOLATIONLEVELREPEATABLEREAD;
STARTTRANSACTION;
SELECT*FROMt_testWHEREid=1;
SELECTSLEEP(10)FROMdual;
SELECT*FROMt_testWHEREid=1;

除了第一行设置事务隔离级别的语句不一样之外,其它部分与读已提交部分的示例一样,所以此处不再详细注释。当然,事务二的结果与读已提交的部分不一样,它两次读取id=1的数据,结果都是John,也就是更新后的数据。

对于可重复读,它不能避免幻读,对于这一部分,我们再用一个例子来进行说明,它产生幻读的示意图如下:

打开网易新闻 查看精彩图片

事务一插入id=2的数据,但还没有提交,这时候事务二读取不到id=2的记录,但紧接着插入id=2的数据又会报错。

事务一的SQL如下:

#设置事务隔离级别为可重复读
SETGLOBALTRANSACTIONISOLATIONLEVELREPEATABLEREAD;
STARTTRANSACTION;
#插入id=2的数据
INSERTINTOt_testVALUES(2,'Jack');
#确认上一步插入成功
SELECT*FROMt_testWHEREid=2;
#让当前进程休眠5秒再提交,以观察事务二的执行情况
SELECTSLEEP(5)FROMdual;
COMMIT;

事务二的SQL:

#设置事务隔离级别为可重复读
SETGLOBALTRANSACTIONISOLATIONLEVELREPEATABLEREAD;
STARTTRANSACTION;
#查询不到id=2的数据
SELECT*FROMt_testWHEREid=2;
#但如果插入id=2的数据则会报错
INSERTINTOt_testVALUES(2,'Jack');
COMMIT;

先执行事务一的SQL,再紧接着执行事务二的SQL,就会发现,在事务二中,虽然查询不到id=2的数据,但该事务向表中插入id=2的记录时却会报错(Duplicate Key),好像出现幻觉一样,所以可重复读存在幻读的问题。

四、串行化

串行化是最高的事务隔离级别,它强制事务按照顺序执行,从而可以避免前面提到的幻读问题,但它会在读取的每一行数据上都加锁,或者每次读取都在一个单独的事务中,总之,这种隔离级别会产生大量的加锁情况,效率很低,实际开发中也较少采用。

打开网易新闻 查看精彩图片

事务一的SQL:

#设置事务隔离级别为串行化
SETGLOBALTRANSACTIONISOLATIONLEVELSERIALIZABLE;
STARTTRANSACTION;
#插入id=2的数据
INSERTINTOt_testVALUES(2,'Jack');
#确认上一步的操作成功
SELECT*FROMt_testWHEREid=2;
#休眠5秒,方便观察
SELECTSLEEP(5)FROMdual;
COMMIT;

事务二的SQL:

#设置事务隔离级别为串行化
SETGLOBALTRANSACTIONISOLATIONLEVELSERIALIZABLE;
STARTTRANSACTION;
#一直阻塞,直到事务一提交,然后读取到id=2的结果
SELECT*FROMt_testWHEREid=2;
COMMIT;

先执行事务一的SQL,紧接着执行事务二的SQL,会发现事务二一直阻塞,直到事务一提交,然后事务二可以读取到新插入的数据,从而避免了幻读的问题。

五、总结

MySQL的事务隔离级别是非常重要的概念,也是相对比较难的技术点,面试中也会经常问题,上面我们实例演示了这四种隔离级别的具体用法和效果,希望对朋友们有所帮助。上面的示例基于MySQL5.7版本,存储引擎为常用的InnoDB。下面用一个图来总结一下这几种隔离级别的情况:

打开网易新闻 查看精彩图片

创作不易,烦请点赞、分享,感谢!