轻松理解Mysql事务的隔离级别和MVCC

参考《Mysql是怎样运行的:从根上理解MYSQL》

事务的隔离级别

事务并发执行遇到的一致性问题

假设有个一致性的需求是,一张表的age字段和adult字段是要对应的,即age大于等于18的时候,adult为成年

  • 脏写(Dirty Write)
    假设现在事务T1和T2并发执行,他们要对表中的age和adult做如下修改:
    1、T1将age字段设置为15
    2、T2将age字段设置为19
    3、T2将adult字段设置为1(假设1代表成年)
    4、T2提交事务
    5、T1将adult字段设置为0
    6、T1提交事务
    
    结果是,age字段值为19,adult字段值为0(未成年),这就违背了我们的age大于18即为成年的一致性需求。单看T1和T2他们两个事务都是各自遵循着age大于18即为成年的一致性需求的,但是如果是并发的执行就有可能会出现违背一致性的情况。我们称:如果一个事务(T2)修改了另一个未提交事务(T1)修改过的数据,就意味着发生了脏写
  • 脏读(Dirty Read)
    假设现在事务T1和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获取到的age和adult数据就已经违背了一致性需求了。我们称:如果一个事务(T2)读取到了另一个未提交的事务所修改的数据,就意味着发生了脏读
  • 不可重复读
    假设现在事务T1和T2并发执行,他们要对表中的age和adult做如下修改:
    假设原本age为15,adult为0
    1、T1读取age的值为15
    2、T2修改age的值为20,adult的值为1,并提交事务
    3、T1读取adult的值为1,提交事务
    
    上述的第三步,T1获取到的age和adult数据就已经违背了一致性需求了。我们称:如果一个事务修改了另一个未提交的事务所读取的数据,就意味着发生了不可重复读的现象。这个例子是为了体现违背一致性所举例的,下面有个更贴切的例子:
    假设原本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,并提交事务
    
    上述T1这个事务三次读取age的值都不一样,即不可重复读。
  • 幻读
    假如有个一致性需求是有个x字段记录这成年人的数量,
    1、T1查询age大于18的数据,得到结果集1
    2、T1查询x的值
    2、T2新增了几条大于18的数据,并提交事务
    3、T1又查了一下大于18的数据,得到结果集2,并提交事务
    
    上述的第三步,T1第二次查询的结果集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表

numbernamecountry
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 COMMITTEDREPEATABLE 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 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能看到的第一个版本。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值