事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。本文对MySQL的四种隔离级别进行了简述,至于锁和隔离级别具体的实现原理会在后续的文章中讲解。
一、MySQL四种隔离级别简述
什么是隔离级别呢?我们都知道,单事务顺序操作数据库时,不论对数据进行什么操作,读到的都应该是数据库中最新的数据。但是大多数系统都不会只在数据库中进行顺序操作,在一个事务A中进行操作时,另一个事务B很可能正在读取同一条数据,那么B应该读取A提交前的数据,还是提交后的数据呢?根据实际业务不同需要,MySQL提供了四种隔离级别供我们选择:
名称 | 含义 |
---|---|
Read Uncommitted | 读取未提交内容 |
Read Committed | 读取提交内容 |
Repeatable Read | 可重读 |
Serializable | 可串行化 |
二、四种隔离级别的区别
准备工作
只需要准备两个MySQL连接终端(简称A、B)和一张account表(在本文中,name字段没有用):
id (pk) | name | balance |
---|---|---|
1 | X | 200 |
1、Read Uncommitted
两个终端内设置并分别启动事务:
//先设置session为RU:
set session transaction isolation level read uncommitted;
//启动事务:
start transaction;
此时B对数据进行更新:
update account set balance=balance-50 where id=1;
在B没有提交的前提下,我们在A终端内进行查询:
select * from account;
得到数据并与之前对比:
id (pk) | name | balance(之前) | balance(现在) | 变化 |
---|---|---|---|---|
1 | X | 200 | 150 | -50 |
对于A终端来说,B终端即使不提交,数据也可以生效。如果此时我们对终端B进行回退处理:
rollback;
就会在A终端内查询到最初始的数据:
id (pk) | name | balance |
---|---|---|
1 | X | 200 |
所以对于A终端来说,RU是可以查到最新的未提交的数据(脏数据),这个级别我们一般不用。
2、Read Committed
将balance恢复到200,然后进行设置:
//先设置session为RU:
set session transaction isolation level read committed;
//启动事务:
start transaction;
此时B对数据进行更新:
update account set balance=balance-50 where id=1;
在B没有提交的前提下,我们在A终端内进行查询:
select * from account;
得到数据并与之前对比:
id (pk) | name | balance(之前) | balance(现在) | 变化 |
---|---|---|---|---|
1 | X | 200 | 200 | 无 |
对于A终端来说,B终端不提交,数据就不会生效。如果此时我们对终端B进行提交处理:
commit;
就会在A终端内查询到生效的数据:
id (pk) | name | balance(之前) | balance(现在) | 变化 |
---|---|---|---|---|
1 | X | 200 | 150 | -50 |
所以对于A终端来说,RC是可以查到最新的已提交的数据,但是不可以重复读(虽然A事务本身没有对数据进行操作,但是数据也会发生变化)。如果你要用confluence或一些其他特定的APP的话,会强制要求数据库是RC级别的。
3、Repeatable Read
将balance恢复到200,然后进行设置:
//先设置session为RU:
set session transaction isolation level repeatable read;
//启动事务:
start transaction;
此时B对数据进行更新:
update account set balance=balance-50 where id=1;
在B没有提交的前提下,我们在A终端内进行查询:
select * from account;
得到数据并与之前对比:
id (pk) | name | balance(之前) | balance(现在) | 变化 |
---|---|---|---|---|
1 | X | 200 | 200 | 无 |
对于A终端来说,B终端不提交,数据就不会生效。如果此时我们对终端B进行提交处理:
commit;
A终端内查询到的数据依然与之前一样:
id (pk) | name | balance(之前) | balance(现在) | 变化 |
---|---|---|---|---|
1 | X | 200 | 200 | 无 |
所以对于A终端来说,虽然RR给出的数据不是最新生效的数据,但是这代表同一事务内的数据可以重复读取,这是InnoDB的默认隔离级别。注意别关掉,我们再尝试一些比较有意思的东西:
在A终端内再对数据进行操作,扣掉50块钱:
update account set balance=balance-50 where id=1;
猜猜现在A终端会是多少钱呢?我们设想,A终端查到的是200块钱,那么扣掉50以后,应该是150对不对?然而结果并不是150,而是100:
id (pk) | name | balance(之前) | balance(现在) | 变化 |
---|---|---|---|---|
1 | X | 200 | 100 | -100 |
也就是说,我们读到的不是最新生效的数据,但是update的时候取出来更新的却是最新生效的数据。如果有兴趣的话,还可以试试同时开启事务后,B终端插入一条id为2的数据并提交,此时A终端虽然不能查询到该条数据,但是当A终端也插入一条id为2的数据的时候,会发生主键冲突。这个原因在于select是受到事务隔离级别控制的,读取记录叫做“快照读”,而insert、update、select for update是直接对记录进行加锁,获取最新生效记录(读取结果等于RC,但是要锁行),叫做“当前读”。
4、Serializable
将balance恢复到200,然后进行设置:
//先设置session为RU:
set session transaction isolation level serializable;
//启动事务:
start transaction;
先在A终端内进行查询:
select * from account;
此时B对数据进行更新:
update account set balance=balance-50 where id=1;
发现在A终端提交之前,B终端进入了等待,是不会返回执行结果的,这就是把事务进行了串行化,好比之前是好几个窗口开展业务,现在只有一个窗口开展业务。虽然克服了幻读、脏读的问题,但是性能上受到非常大的影响,因此日常业务中使用的也比较少。