事务的四大特性为原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),本篇专门说说隔离性。
四大隔离级别
在多个事务同时执行时就会出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决该问题引入了”隔离级别“的概念。
以下四种隔离级别刚好解决三种“读的问题”,隔离级别从轻到重为:
读未提交:一个事务做了修改还未提交就会被其他事务所读到。
读提交:一个事务提交之后其做的改变才能被其他事务读到。
可重复读:一个事务执行过程中看到的数据总是和其刚开始时看到的相同。
串行化:顾名思义,各个事务串行执行。
mysql的隔离级别为可重复读,oracle为读提交
如下以一个经典的例子说明这三种读问题和四种隔离级别
代码语言:javascript复制事务一 事务二
启动事务,查询发现为1 启动事务
将值由1改为2
查询得到V1
提交事务二
查询得到V2
提交事务一
查询得到V3
读未提交的隔离级别下,v1值为2,他读到了事务二还未提交之前的数据,这种现象称之为脏读;
读提交的隔离级别下,v1值为1,v2值为2。事务一执行期间之后读的数据和刚开始的数据不同,这种现象称之为不可重复读;
可重复读的隔离级别下,v1值为1,由于可重复读保证了前后读到的数据相同因此v2值为1,v3值为2。
串行化的隔离级别下,依然v1 = 1, v2 = 1, v3 = 2。
到这好像也没发现串行化比可重复读更高隔离级别的好处,
代码语言:javascript复制事务一 事务二
启动事务,
查询c字段发现不存在
启动事务
插入c字段
提交事务二
查询c字段
插入c字段
提交事务一
在上述案例中隔离级别若为可重复读时,发现事务一中第二次查询c字段发现还是不存在(这是又可重复读决定的),但是插入时缺无法插入,对事务一而言明明不存在我想插入缺不能插入,跟见了鬼一样。
使用串行化隔离级别就可以避免该问题,只有事务一执行完成后才能执行事务2,此时事务二发现c字段存在。
事务隔离的实现
实现上,数据库会创建一个视图。“可重复读”隔离级别下,这个视图是在事务启动时创建的;在“读提交”隔离级别下在每条sql语句开始前创建一个视图;“读未提交”直接返回最新值为视图概念;“串行化”隔离级别下直接用加锁的方式避免并行访问。
在MySQL中,每条记录在更新的时候会记录一条回滚操作,由当前值回滚即可得到其上一状态值,如下图所示,若当前值由1改成2,再改成3,最后改成4,其回滚段如下:
上图来自mysql45讲
当系统中没有比回滚日志更早的read-view时,回滚日志才会删除。因此经量避免使用长事务。
事务的启动方式:
1、显式启动事务
代码语言:javascript复制// autocommit= 1 的情况
begin // 显式启动事务
commit // 提交事务
rollback // 回滚
2、set autocommit=0 // 默认为1
如此只要执行一个select语句,事务就启动了,其并不会自动提交直到主动执行commit或rollback。
推荐使用方式一,对于方式一比方式二对一次交互的问题,使用commit work and chain,提交当前事务并且开始下一个事务(相当于 commit begin)
事务到底是隔离还是不隔离?
该节以一个案例引入,如下情况都是在可重复读的隔离级别下的
假设表t中id = 1时 k字段初始值为1
代码语言:javascript复制事务A开启
事务B事务开启
select k from id = 1
事务C事务开启
update t set k = k 1 where id = 1
提交事务
update t set k = k 1 where id = 1
select k from t where id = 1
提交事务
select k from t where id = 1
提交事务
事务A查询到k的值仍然为1,事务B查询到第一次查询到的值为1, 第二次查询到的k值竟然为3。不是说可重复读嘛,咋不重复了,就算要变也是变为2啊?
InnoDB中每条事务都有自己的id号,其在事务开始时向InnoDB事务系统申请的,按照申请顺序严格递增。
此外每行数据也有多个版本,每次事务更新数据后都会生成一个新的数据版本,并把当前的事务id赋给这个数据版本的事务id,因此访问数据时可以通过判断当前事务id和当前数据版本事务id的大小,若当前数据版本的事务id小于当前行的事务id,则说明该数据在本事务中是可见的,否则任务当前数据版本是本事务开启后别的事务修改后的版本,往前回退直到得到满足条件的数据版本。
数据的更新操作都是先读后写的,这个读指的就是当前读(即读当前最新版本的数据而不是读小于等于当前事务id 的数据)。
因此事务B修改操作读到的是事务C提交的k值2,加1就变为3了。
再来一个问题
代码语言:javascript复制 事务B事务开启
select k from id = 1
事务C事务开启
update t set k = k 1 where id = 1
update t set k = k 1 where id = 1
select k from t where id = 1
提交事务
提交事务
其结果与之前相同。
事务B执行更新语句之前,事务C先拿到行锁,并更新完,此时到事务B后,由于其拿不到锁一直阻塞,直到事务C提交事务了(用到两阶段锁协议),然后事务B才能拿到行锁继续执行。
可重复读和读提交的主要区别
可重复读的隔离级别下,只需事务开始时创建一个一致性视图,之后事务中其他查询都使用该视图。
读体检隔离级别下,每一条语句执行之前都会重新计算出一个新视图。