来广营做网站,wordpress微商模板,wordpress文章版权声明,会员wordpress主题文章目录 概要一、基于UNDO LOG的版本链1.1、行记录结构1.2、了解UNDO LOG1.3、版本链 二、Read View2.1、判定机制2.2、源码 三、参考 概要
在上文中#xff0c;我们提到了MVCC#xff08;Multi-Version Concurrency Control)多版本并发控制#xff0c;是通过undo log来实… 文章目录 概要一、基于UNDO LOG的版本链1.1、行记录结构1.2、了解UNDO LOG1.3、版本链 二、Read View2.1、判定机制2.2、源码 三、参考 概要
在上文中我们提到了MVCCMulti-Version Concurrency Control)多版本并发控制是通过undo log来实现的。那具体是如何实现的呢将在本文一一道来。
MVCC是为了实现非阻塞读即提高数据库并发读能力的一种机制。 通常来说A事务正在修改数据行X在修改未结束前B事务要读数据行X,为了避免读到脏数据B就会被阻塞,直到A事务修改完数据行XMVCC很好的避免了这种情况的发生。 MVCC是通过保存数据在某个时间点的快照来实现的即保存一个数据行的多个变更版本空间换时间。这些版本就是undo log了每一行的变更记录就存在undo log中通过链表联系在一起构成了一个完整的版本链供MVCC实现非阻塞读。 例如在可重复读隔离级别下A事务正在修改数据行X在修改数据行X前会把其当前记录插入到版本链中B事务要读数据行X就到版本链中找符合的这样B就不会被阻塞了。 ps:MySQL的MVCC只作用于在REPEATABLE READ和READ COMMITED两个隔离级别下执行普通的SELECT操作。
在高性能MySQL第三版一书中对MVCC的操作描述如下 下面来一起探索下具体实现吧。
本文背景MySQL InnoDB存储引擎。
一、基于UNDO LOG的版本链
在了解版本链之前首先看一下InnoDB存储引擎的行记录。
1.1、行记录结构
提到MySQL的行记录肯定会想到行ID、用户数据列等内容除了这些信息外还有一些隐藏信息比如事务ID、回滚指针等其他额外信息那我们可以得出下图 其中事务ID(trx_id)、回滚指针(rollback_ptr)是本文要讲的核心。
ps:InnoDB的行记录是存储在聚族索引中的
1.2、了解UNDO LOG MySQL undo log结构示意图MySQL的undo log分为两大类
insert undo:insert 操作产生的记录了table_id、trx_id、主键各列数据等信息。update undo:update和delete操作产生的 虽说update和delete操作产生的undo log都会记录到update undo这个大类但其记录内容是有很大差距的。 delete操作产生的undo log会记录table_id、trx_id、rollback_ptr、主键各列数据等信息而update操作产生的undo log会记录更新table_id、trx_id、rollback_ptr、被更新列旧值、主键各列数据等信息。 ps:可以看到undo log中并没有记录用户列数据
1.3、版本链
我们现在在test库下有一个test表 下面我们经过一系列插入删除更新来演示版本链的变迁 假设当前全局trx_id 101。
插入一条数据
insert into test (id,num,name,key_id) values (1,1,bob,11);此时有
更新数据 1令id1的数据nametom
update test set name tom where id 1;此时有 2令id1的数据namejoin
update test set name join where id 1;此时有
删除数据删除id1的数据
delete from test where id 1;此时有 如上图在对id1这一条记录的插入更新删除的过程中构建了一个版本链。其中删除操作只是在聚簇索引上的记录中打了删除标记并不会立即删除而是当没有Read View持有该事务ID时才会有purge线程去真的去删除之后这块空间才能被使用为什么不能立即删除呢主要是因为undo log中并不保存所有的用户列数据甚至不保存都是基于聚簇索引中的记录行在结合undo log内容在回滚过程中构造某个版本的数据。
另外我们在1.2小节中强调了 undo log中并不记录用户列数据这里只是为了表示方便才画了出来其实MySQL是基于聚簇索引上的记录内容通过从聚簇索引上的记录roll_ptr开始依次回滚直到遇到符合要求的事务ID构造出最终数据。
假设第二次更新操作开始但未提交时有个trx_id107的事务要读id1的数据此时发现ID1的数据trx_id106且处于活跃状态则需要沿着版本链回滚当遇到trx_id102的记录结合聚簇索引上的记录和trx_id102的undo记录,构造出trx_id102的完整数据对于trx_id107的事务来说该事务是已提交的事务则读取即可。其实这就是Read View了详情请看下一章节。
二、Read View 对于READ COMMITED隔离级别需要读已经提交的数据那当A事务修改完聚簇索引上的记录X后尚未提交此时B事务读取记录X按照定义此时聚簇索引上的记录是不允许读取的如何判定呢就依赖Read View机制了 对于REPEATABLE READ隔离级别需要重复读数据那当A事务读取记录X后B事务修改完聚簇索引上的记录X并提交此时A事务需要在此读取记录X按照定义此时聚簇索引上的记录是不允许读取的如何判定呢也就依赖Read View机制了 Read View也称作一致性视图其主要包含4个主要的内容
m_ids在生成Read View时当前系统中活跃的读写事务的事务ID列表min_trx_id在生成Read View时当前系统中活跃的读写事务中最小的事务ID即m_ids中的最小值max_trx_id在生成Read View时当前系统中活跃的读写事务中最大的事务ID即系统应该分配给下一个事务的事务ID全局事务ID的值creator_trx_id在生成Read View时当前事务的事务ID。
其中max_trx_id要注意下并不一定是m_ids中的最大值而是生成Read View时的全局事务ID值。因为事务ID时递增循环分配的在RR隔离级别下假设当前活跃的事务ID有1,2,3事务ID3的事务提交后再开启一个事务A发起读操作此时Read View时m_ids[1,2,4],min_trx_id1max_trx_id4,如果有另一个写事务B提交了消耗了一个事务ID4那么此时事务A进行写操作就会出现creator_trx_id5的情况。
为什么会这样呢我们要明白事务ID的生成并不是开启事务执行begin操作时就确定的而是第一次执行写操作时确定的。 而Read View生成时机是在读操作前确定的但RC与RR还不同RC是每次读操作前都生成一个Read View保证可以读已提交数据而RR是在第一次读操作前生成一个Read View就不会变动了保证可重复读。
2.1、判定机制
MySQL根据Read View读要访问的记录依次进行以下判定来决定是否可访问
如果被访问记录的trx_id等于creator_trx_id相等这意味当前事务在访问它自己修改的记录允许被访问如果被访问记录的trx_id小于min_trx_id这意味被访问记录在当前事务生成Read View时已经提交了允许被访问如果被访问记录的trx_id大于等于max_trx_id,这意味被访问记录在当前事务生成Read View之后产生的不允许被访问如果被访问记录的trx_id在m_ids中说明在当前事务生成Read View时被访问记录所属的事务还是活跃的不允许被访问如果被访问记录的trx_id不在m_ids中说明在当前事务生成Read View时被访问记录所属的事务已经提交了允许被访问
以上的判定机制是实现RC和RR的基础。
select trx_id,trx_state,trx_started,trx_rows_locked from INFORMATION_SCHEMA.INNODB_TRX; #可以查看当前活跃的事务id等信息
针对1.3小节的版本链案例 我们依次执行下面四个语句看看效果如何(RR隔离级别) 1:
insert into test (id,num,name,key_id) values (1,1,bob,11);#事务id 1012:
begin;
update test set name tom where id 1; #事务id 1023:
insert into test (id,num,name,key_id) values (2,2,2ob,22);#事务id 103
insert into test (id,num,name,key_id) values (3,3,3ob,33);#事务id 1044
begin;
update test set name uuu where id 2; #事务id 1055:
begin;
select * from test where id 1;
update test set name uuu where id 3; #事务id 106那么第五句select * from test where id 1;的Read View如下 m_ids[102,105]min_trx_id102max_trx_id106, creator_trx_id 0
针对id1这条记录此时test表聚簇索引中的记录是 (1,1,tom,11)隐藏字段trx_id102。但是102在m_ids中所以不可见根据其undo log得到最终结果 (1,1,bob,11)
2.2、源码
MySQL V8.0.32
生成Read View
/*调用链如下
trx_assign_read_viewtrx_sys-mvcc-view_open(MVCC::view_open)view-prepare(ReadView::prepare)*///下面来看看核心的prepare函数干了什么/*
Opens a read view where exactly the transactions serialized before this
point in time are seen in the view.
param id Creator transaction id */
void ReadView::prepare(trx_id_t id) {ut_ad(trx_sys_mutex_own());m_creator_trx_id id; //赋值creator_trx_idm_low_limit_no trx_get_serialisation_min_trx_no(); m_low_limit_id trx_sys_get_next_trx_id_or_no();//系统应该分配给下一个事务的事务ID即max_trx_id ut_a(m_low_limit_no m_low_limit_id);if (!trx_sys-rw_trx_ids.empty()) { copy_trx_ids(trx_sys-rw_trx_ids); //将此刻全局活跃跃事务列表ids 赋值给当前READ VIEW的m_ids属性} else {m_ids.clear();}/* The first active transaction has the smallest id. */m_up_limit_id !m_ids.empty() ? m_ids.front() : m_low_limit_id;//获取当前活跃事务列表中的最小事务id,即min_trx_idut_a(m_up_limit_id m_low_limit_id);ut_d(m_view_low_limit_no m_low_limit_no);m_closed false;
} Read View判定 /** Check whether the changes by id are visible.param[in] id transaction id to check against the viewparam[in] name table namereturn whether the view sees the modifications of id. */[[nodiscard]] bool changes_visible(trx_id_t id,const table_name_t name) const {ut_ad(id 0);if (id m_up_limit_id || id m_creator_trx_id) {//小于min_trx_id 或等于 creator_trx_id 则允许访问return (true);}check_trx_id_sanity(id, name);if (id m_low_limit_id) { //大于max_trx_id 则不允许访问return (false);} else if (m_ids.empty()) {//m_ids为空则允许访问return (true);}const ids_t::value_type *p m_ids.data();return (!std::binary_search(p, p m_ids.size(), id));//二分查找在m_ids不允许访问不在则允许访问}三、参考
1]:庖丁解InnoDB之Undo LOG 2]:正确的理解MySQL的MVCC及实现原理