参考《Mysql是怎样运行的:从根上理解MYSQL》
事务的隔离级别
事务并发执行遇到的一致性问题
假设有个一致性的需求是,一张表的age字段和adult字段是要对应的,即age大于等于18的时候,adult为成年
。
- 脏写(Dirty Write)
假设现在事务T1和T2并发执行,他们要对表中的age和adult做如下修改:
结果是,age字段值为19,adult字段值为0(未成年),这就1、T1将age字段设置为15 2、T2将age字段设置为19 3、T2将adult字段设置为1(假设1代表成年) 4、T2提交事务 5、T1将adult字段设置为0 6、T1提交事务
违背了
我们的age大于18即为成年的一致性需求。单看T1和T2他们两个事务都是各自遵循
着age大于18即为成年的一致性需求的,但是如果是并发的执行就有可能会出现违背一致性的情况。我们称:如果一个事务(T2)修改了另一个未提交事务(T1)修改过的数据,就意味着发生了脏写
。 - 脏读(Dirty Read)
假设现在事务T1和T2并发执行,他们要对表中的age和adult做如下修改:
上述的第二步,T2获取到的age和adult数据就已经违背了一致性需求了。我们称:假设原本age为15,adult为0 1、T1将age改成20 2、T2获取age为20,adult为0 3、T1将adult改成1 4、提交T1、T2
如果一个事务(T2)读取到了另一个未提交的事务所修改的数据,就意味着发生了脏读
。 - 不可重复读
假设现在事务T1和T2并发执行,他们要对表中的age和adult做如下修改:
上述的第三步,T1获取到的age和adult数据就已经违背了一致性需求了。我们称:假设原本age为15,adult为0 1、T1读取age的值为15 2、T2修改age的值为20,adult的值为1,并提交事务 3、T1读取adult的值为1,提交事务
如果一个事务修改了另一个未提交的事务所读取的数据,就意味着发生了不可重复读的现象
。这个例子是为了体现违背一致性所举例的,下面有个更贴切的例子:
上述T1这个事务三次读取age的值都不一样,即不可重复读。假设原本age为15,adult为0 1、T1读取age的值为15 2、T2修改age的值为20,adult的值为1,并提交事务 3、T1读取age的值为20 4、T3修改age的值为30,adult的值为1,并提交事务 5、T1读取到的age值为30,并提交事务
- 幻读
假如有个一致性需求是有个x字段记录这成年人的数量,
上述的第三步,T1第二次查询的结果集2比第一次查询到的结果集会多了几条。我们称:1、T1查询age大于18的数据,得到结果集1 2、T1查询x的值 2、T2新增了几条大于18的数据,并提交事务 3、T1又查了一下大于18的数据,得到结果集2,并提交事务
如果一个事务(T1)先根据某些条件查询出一些记录,在该事务未提交时,另一个事务(T2)写入了一些符合这个查询条件的数据,就意味着发生了幻读
。幻读也可能发生违背一致性的问题,上述T1查询到的x的值和结果集2的数量就对不上了。
根据上节介绍的并发事务执行过程中可能会发生的一些现象,这些现象对事务的一致性产生不同程度的影响,我们按照可能导致的一致性问题的严重性排了个序:
脏写 > 脏读 > 不可重复读 > 幻读
4种隔离级别
- READ UNCOMMITED:读未提交
- READ COMMITED:读已提交
- REPEATABLE READ:可重复读
- SERIALIZABLE:可串行化
四种隔离级别能够避免的一致性问题
脏写 | 脏读 | 不可重复读 | 幻读 | |
---|---|---|---|---|
READ UNCOMMITED | √ | × | × | × |
READ COMMITED | √ | √ | × | × |
REPEATABLE READ | √ | √ | √ | × |
SERIALIZABLE | √ | √ | √ | √ |
ps: 在MVCC下, REPEATABLE READ可以很大程度上避免幻读。
Mysql设置隔离级别
mysql默认的隔离级别是REPEATABLE READ。
- 全局设置
只对执行完该语句之后新产生的会话起作用,对当前已经存在的会话无效。
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIABLIZABLE | READ UNCOMMITED | READ COMMITED | REPEATABLE READ;
或
SET @@GLOBAL.transaction_isolation = SERIABLIZABLE | READ UNCOMMITED | READ COMMITED | REPEATABLE READ;
- 会话设置
对当前会话后续的事务有效。
SET SESSION TRANSACTION ISOLATION LEVEL SERIABLIZABLE | READ UNCOMMITED | READ COMMITED | REPEATABLE READ;
或
SET @@SESSION.transaction_isolation = SERIABLIZABLE | READ UNCOMMITED | READ COMMITED | REPEATABLE READ;
或
SET transaction_isolation = SERIABLIZABLE | READ UNCOMMITED | READ COMMITED | REPEATABLE READ;
- 下一个事务
只对下一个事务有效。
SET @@transaction_isolation = SERIABLIZABLE | READ UNCOMMITED | READ COMMITED | REPEATABLE READ;
ps: mysql5.7之前transaction_isolation换成tx_isolation
MVCC(多版本并发控制)
版本链
对于InnoDB引擎来说,它的聚簇索引记录中都包含下面连个必要的隐藏列:
- trx_id:一个事务每次对某条聚簇索引记录做修改时,都会把该事务的事务id赋值给trx_id这个隐藏列;
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中。这个隐藏列相当于一个指针,可以通过它找到该记录修改前的值。
假设有一张hero表
number | name | country |
---|---|---|
1 | 刘备 | 蜀 |
假设插入该记录的事务id为80,那么此刻该条记录的示意图如下:
这个insert undo表示该条记录的最新一次改动是新增,也就是说这条记录是刚新增进来的。
后续假设有一个事务100和事务200对该条数据做如下改动:
-- 事务100(T100)
BEGIN;
UPDATE hero SET name='关羽' WHERE number = 1;
UPDATE hero SET name='张飞' WHERE number = 1;
COMMIT;
-- 事务200(T200)
BEGIN;
UPDATE hero SET name='赵云' WHERE number = 1;
UPDATE hero SET name='诸葛亮' WHERE number = 1;
COMMIT;
每一次对记录的改动,都有记录一条undo日志,每条undo日志也有一个roll_pointer属性(除了INSERT类型的undo日志,因为它之前不可能有事务对该数据改动)。可以通过这个roll_pointer属性将undo日志串成一个链表,如下:
每次更新后,都会将旧值放到一个undo日志中(算是该记录的一个旧版本),随着次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,这个链表就是版本链。我们之后会利用这个版本链来控制并发事务访问相同记录时的行为,我们把这种机制称之为多版本并发控制(Muti-Version Concurrency Control,MVCC)
。
ps:其实UPDATE操作产生的undo日志中,只会记录一些索引列以及修改的列,并不会记录所有列的信息,上面画出来只是为了方便理解。
Read View
对于READ UNCOMMITED
隔离级别的事务来说,每次读取记录的最新
的版本就行了。对于SERIALIZABLE
来说是通过加锁
的方式实现的,这个后续博客会写。对于使用READ COMMITTED
和REPEATABLE READ
隔离级别的事务,我们读取版本链上的哪条数据就得依靠Read View
了。这个Read View包含以下四个内容:
- m_ids:在生成Read View的时,当前系统中活跃的事务id列表。
- min_trx_id:在生成Read View的时,m_ids中的最小值。
- max_trx_id:在生成Read View的时,系统应该分配给下一个事务的事务id值(不是已经分配的事务id的最大值)。
- creator_trx_id:生成该Read View的事务的事务id
有了这个Read View之后,在访问某条记录时候,只要按照以下步骤判断你能读取哪个版本的数据记录:
- 如果被访问的数据的
trx_id和creator_trx_id相同
, 相当于事务访问它自己修改过的记录,当前事务自然能
访问该记录。 - 如果被访问的数据的
trx_id小于min_trx_id的值
,相当于生成该版本数据的事务在当前事务生成Read View之前就已经提交事务了,那么该版本的记录也可以
被访问。 - 如果被访问的数据的
trx_id大于等于max_trx_id的值
,相当于生成该版本数据的事务在当前事务生成Read View之后生成的,那么自然是访问不了
的。 - 如果被访问的数据的
trx_id小于max_trx_id且大于min_trx_id
,则就要看该事务id是否在m_ids里面
了,如果在
,则相当于生成该版本数据的事务在当前事务生成Read View时还没有提交,那么是取不到该版本的记录
,如果不在
,则可以访问
。
如果某个版本的数据对当前事务不可见,则会顺着版本链往更旧一点的版本里面去找
。
在Mysql中,READ COMMITED 和 REPEATABLE READ中的区别就是生成READ VIEW的时机不同,- READ COMMITED,在
每次
执行select语句的时候就会生成一个READ VIEW。 - REPEATABLE READ,在
第一次
执行select语句的时候才会生成一个READ VIEW。
- READ COMMITED,在
假设有个READ COMMITTED隔离级别的事务id为150的事务,执行以下语句
-- 假设上述的T100事务还没提交
SELECT hero WHERE number = 1;-- 生成的READ VIEW的m_ids列表里面就有100的值。则查到的数据则是刘备那一条
--假设上述的T100事务已经提交
SELECT hero WHERE number = 1;-- 生成的READ VIEW的m_ids列表里面里面就没有100的值且事务id一定小于max_trx_id,则查到的数据为张飞那一条
如果是REPEATABLE READ级别的事务执行上述语句的话,由于只会在第一次select的时候生成READ VIEW,那么两次查询到的结果应该都是刘备那一条数据,这样就不会出现不可重复读的情况了。根据这个我们也可以知道为什么REPEATABLE READ级别的事务能很大程度上避免幻读情况的产生了,感兴趣的可以自己实验分析一下~
二级索引与MVCC
因为只有聚簇索引的记录上才有trx_id和roll pointer,如果只查询二级索引上的数据呢?(不理解的同学可以去了解下索引覆盖),假设name字段是普通索引字段,如下sql
select name where name = '刘备';
判断该记录是否可见大致分为以下两步:
- 步骤一:二级索引页面中有个PAGE_MAX_TRX_ID字段,记录着操作当前页面里面的数据的最大的那个事务id,如果生成READ VIEW的min_trx_id大于这值,那么所以该页面的所有记录都对该READ VIEW可见。否则就执行第二步。
- 步骤二:利用二级索引记录的主键值进行回表操作,得到对应的聚簇索引的就后再按照前面的步骤查询READ VIEW能看到的第一个版本。