不同问题的解决方案
在上次(MySQL事务详解(一):并发问题与解决之道)的分享学习中提到,MySQL根据事务并发导致的问题的严重程度,分别制定了不同的隔离级别来规避问题。
针对严重性最高的脏写
,MySQL使用锁机制
来解决掉。脏读、不可重复读、幻读都用不同的隔离级别与之对应。
而在不同的隔离基本,底层实现又有着差异:
- • 隔离级别最高的
可串行化
,底层也是使用锁机制
来实现。 - •
已提交读、可重复读
,底层使用的是MVCC(多版本并发控制)
实现。 - • 隔离级别最低的
未提交读
,只需直接读取最新数据即可。 如下图:
针对锁,会在以后得分享中学习,首先来学习MVCC。
版本链
学习MVCC,首先要知道版本链。
版本链的作用
- • 用于回滚操作:当要撤销对数据库的影响时,就需要
用之前的数据覆盖修改后的数据
。这就需要数据库中的每条数据存在多个不同的副本(即多个版本)
用于恢复。 - • 用于事务隔离:保证不同事务修改的数据是不同版本的,查看的数据是符合当前隔离级别的数据。即
保证事务隔离不影响
。
版本链的结构
为了保证版本链满足以上两个作用,每条数据都会被插入几个隐藏列,我们比较关心的是DB_TRX_ID
和DB_ROLL_PTR
。
- • DB_TRX_ID:
修改该条数据的事务ID
。 - • DB_ROLL_PTR:
回滚指针,指向这条数据的上一个版本
。每当该数据被修改,旧版本数据就会被写入UNDO日志
,新数据使用该指针执行旧数据。
基于以上两个隐藏列,版本链就可以实现回滚和事务隔离的功能,结构如图:
ReadView
了解完版本链,再来了解ReadView(读视图)。
undo日志用来保证回滚,那么ReadView就是用来保证事务隔离
,用来判定当前数据对当前的事务是否可见。
ReadView中重要的概念如下:
- • m_ids:一张用来维护生产当前ReadView时,系统中正活跃的事务;
- • min_trx_id : 即m_ids中的最小值;
- • max_trx_id : 系统下一个分配的事务id,即未来事务的id(对当前事务来说);
- • creator_trx_id : 生成当前ReadView的事务id。
根据访问的数据中的trx_id与上述的关系,就可以判断当前版本的数据对当前事务是否可见,关系如下图:
当访问的trx_id = creator_trx_id
,表示正在访问自己当前事务的数据,可见
;
当访问的trx_id < min_trx_id
,表示访问的数据已提交,可见
;
当访问的trx_id > max_trx_id
,表示访问的数据对当前事务来说是未来事务(即在生成当前ReadView之后生成的事务),不可见
;
当min_trx_id < 访问的trx_id < max_trx_id
,就需要判断当前trx_id是否在m_ids列表中,在列表中
表示该事务依然是活跃的,不可见
;不在列表中
表示该事务已提交,可见
。
如果访问的数据不可见的话,就沿着版本链依次按照上述的规则查找符合的数据
,如果未找到可见数据,说明数据库不包含该数据。
隔离级别与ReadView
READ COMMITTED和REPEATABLE READ,都是用了ReadView来实现事务隔离,而两者的区别在于生成ReadView的时机不同
。
- • READ COMMITTED:每次读取数据前都会生成一个ReadView。
- • REPEATABLE READ:只会在第一次查询数据时生成ReadView。
MVCC详述
READ COMMITTED
假设有如下图所示的两个事务以及对应的版本链:
现在使用隔离级别为READ COMMITTED的事务
去查询select number from acount where no = 1;
,得到的结果为110。
过程如下:
1,执行select 生成ReadView:m_ids=[10, 9],min_trx_id = 9, max_trx_id = 11,creator_trx_id = 0(单独的查询语句事务id为0);
2,从版本链开始查找:
第一条trx_id = 10,在m_ids中,不可见,根据指针跳转;
第二条trx_id = 9,在m_ids中,不可见,根据指针跳转;
第三条trx_id = 8 < min_trx_id,表示事务已提交,可见;
最后返回的number = 110;
当T9事务提交后,如下图所示:
现在再使用上述的查询事务去查询select number from acount where no = 1;
,得到的结果为90。
过程如下:
1,行select 重新生成ReadView:m_ids=[10],min_trx_id = 10, max_trx_id = 11,creator_trx_id = 0;
2,从版本链开始查找:
第一条trx_id = 10,在m_ids中,不可见,根据指针跳转;
第二条trx_id = 9 < min_trx_id,表示事务已提交,可见;
最后返回的number = 90;
EPEATABLE READ
依然使用如下图所示的事务与版本链:
现在使用隔离级别为EPEATABLE READ的事务
去查询select number from acount where no = 1;
,得到的结果为110。
过程如下:
1,执行select 生成ReadView:m_ids=[10, 9],min_trx_id = 9, max_trx_id = 11,creator_trx_id = 0;
2,从版本链开始查找:
第一条trx_id = 10,在m_ids中,不可见,根据指针跳转;
第二条trx_id = 9,在m_ids中,不可见,根据指针跳转;
第三条trx_id = 8 < min_trx_id,表示事务已提交,可见;
最后返回的number = 110;
当T9事务提交后,如下图所示:
现在再使用上述的查询事务去查询select number from acount where no = 1;
,得到的结果为110。
过程如下:
1,执行select 不会重新生成ReadView,依然使用上次查询的ReadView:m_ids=[10, 9],min_trx_id = 9, max_trx_id = 11,creator_trx_id = 0:
2,从版本链开始查找:
第一条trx_id = 10,在m_ids中,不可见,根据指针跳转;
第二条trx_id = 9,在m_ids中,不可见,根据指针跳转;
第三条trx_id = 8 < min_trx_id,表示事务已提交,可见;
最后返回的number = 110;
会发现两次查询返回的结果是一致的,这就是可重复读的含义
。
也就清晰的解释了READ COMMITTED和REPEATABLE READ在生成ReadView时机上的差别,导致的不同的查询结果,解决不同的并发问题。