1,前置知识回顾
1.1,当前读和快照读
根据前面章节可知,mysql在【可重复读】(RR)的事务隔离机制下,同一个事务中的多次读取结果是一样的。而【读已提交】(RC)事务隔离机制,每次读取都会读取到已提交事务的最新数据。【读未提交】(RU)事务隔离机制,可以直接读取到未提交事务的数据。
工作中常用的两种隔离机制【RR】(mysql默认)是如何保证同一事务中每次读取结果都一样呢?【RC】又是如何做到读取已提交事务的数据的呢?一切都源于MVCC(Multi-Version Concurrent Control多版本并发控制)的数据可见性原则实现。本章将通过实际案例讲解MVCC数据的可见性机制,分析RR和RC隔离机制是如何追溯数据的。
先回顾两个名词,【快照读】和【当前读】
快照读:RR机制下,第一次查询会生成一个当前时间节点的数据快照,后续该事务中的所有数据查询,都读取当前快照的数据,保证了数据的一致性。
当前读:RR机制下,第一次查询后,当前事务T未结束前,如果有其他事务对某个数据A做了修改并提交了事务,事务T需要对数据A做update,事务T需要获取数据A的最新结果,以防止脏读和保证数据的一致性。
即然如此,RR机制下mysql是不是对每个查询事务都生成一个快照呢?原则上是可以通过这种方式实现,但这种方式快照过多,会对数据库资源造成极大的浪费,且事务结束后需删除无用快照,I/O过多也会占用大量资源。由此引出实现快照读的另外几个名词:【undo log】(回滚日志)、【undo log版本链】、【read-view】(一致性视图)。下面根据案例解释这几个名词,并在案例中讲解快照读的实现原理。
1.2,如何查询事务id
案例开始前,还需再次回顾上一章内容,如何查询mysql事务id:
SELECT * FROM information_schema.INNODB_TRX;
1.3,mysql表隐藏字段
本章节案例还是使用前面章节使用的表t_account,表数据如下
表中可见只有三个字段:id, name, balance. 实则mysql添加了两个隐藏字段:trx_id(事务id),roll_pointer(回滚指针)。案例中将讲解这两个字段的作用。
2,案例讲解MVCC原理
案例的事务操作顺序很重要,顺序错误将导致不同的结果,思维如果对顺序的概念不强也很难理解,所以每步操作和顺序都将命名且排序。
操作一:事务A开启事务,查询当前ID = 4 的数据,即原数据。并查询当前事物ID,即已提交的事务ID = 421991945340720。
操作二:事务B开启事务,修改id=4的数据,更改balance = 5,不提交事务。查询当前事物B的事务ID = 103386,如下图:
操作三:事务C开启事务,查询id=4的数据,不提交事务。查询事务C的事务id = 421991945341624,如下图:
操作先暂停。根据前面的知识,此时事务C查询id=4的数据,balance = 6000.
当前操作流程可用下图表示:
【undo log】:用来回滚 增/删/改 操作的回滚日志。如:事务insert一条数据,则该事务的【undo log】就是delete语句;
【trx_id】:当前操作的事务ID;
【roll_pointer】:当前事物指向【undo log】的指针,表示当前事务如果回滚,将执行哪个【undo log】语句。如上图,事务A修改数据,则事务A的【roll_pointer】指针指向原数据,如果事务A回滚,id = 4的数据将回滚到原数据。事务B时查询事务,不用回滚,所以没有【undo log】.
【undo log版本链】:是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会生成并保留undo log,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链,如上图的黑色箭头。
MVCC的数据可见性是如何实现可重复读的呢?
事务B在读取数据时,获取了当前时间节点的【read-view】,【read-view】的查找逻辑如下:
1,在当前查询的时间节点,查询数据库所有未提交的事务ID,放在数组中。当前案例数据:
uncommitted_trx_ids[] = {103378};
2,获取当前已提交的最大事务ID。当前案例数据:max_trx_id = 421991945340720。
事务C的可重复读,是通过当前事务中数据的可见性实现的。数据可见性实现原理:
1,数据操作执行顺序是按照上图中绿色箭头,依次往下执行,事务ID递增;2,事务里的任何sql查询结果,需要从当前对应版本链里的最新数据(即当前事务ID),开始逐条向上(按上图红色箭头顺序)跟【read-view】做比对,从而得到最终的快照结果。
当前案例比对过程:
1,查询时获取当前事务C的【read-view】: uncommitted_trx_ids[] = {103378},max_trx_id = 421991945340720
2,按照上图中红色箭头依次向上获取事务ID,与当前事务的【read-view】数据作比对。向上比对一条,即比对事务B的trx_id = 103378。事务ID在当前所有未提交事务数组中,该数据不可见;
3,向上比对一条,即比对事务A的trx_id = 421991945340720。事务ID是当前最大已提交事务,数据可见,获取该事务ID对应的数据,即 balance = 6000.
总结:
【可重复读】:查询时获取一次【read-view】,所有未提交事务的数据不可见,最大已提交事务数据可见。由此保证了可重复读。
【读已提交】:每次查询时都获取最新的【read-view】,所有未提交事务的数据不可见,最大已提交事务数据可见,保证了每次提交的最新数据都可见。