1. undo log链
每条undo log 日志都有两个隐藏的字段,一个是 trx_id,一个是 roll_pointer,这个 trx_id 就是最近一次更新这条数据的事务id,roll_pointer 就是指向你更新这个事务之前生成的undo log。
举个例子,假设有一个事务A(id=50),插入一条数据,那么此时这条数据的隐藏字段txr_id 就是50,roll_pointer 指向一个空的 undo log,因为之前这条数据库是没有的。
接着假设有一个事务B跑来修改了这条数据,把值改成值B,事务B的id是58。那么更新之时会生成一个undo log 记录之前的值,即A事务的值。然后B的roll_pointer 指向这个undo log 记录。
接着假设事务C又来修改一下这个值为值C,他的事务id 是 69,此时会把数据行里的 txr_id 改成 69,然后生成一条 undo log,记录之前事务B修改的那个值。
2. ReadView 机制
ReadView 里面比较关键的东西有 4 个。
- m_ids,这个就是说此时哪些事务在 MySQL 里执行还没提交的
- min_trx_id,就是 m_ids 里最小的值
- max_trx_id,这是是mysql 下一个要生成的事务id,就是最大事务id;
- creator_trx_id,就是这个事务id
举个例子,如下。
假设原来数据库里就有一行数据,很早以前就有事务插入过,事务id 是32,他的值就是初始值。
此时两个事务并发过来执行了,一个事务A(id=45),一个事务B(id=59),事务B是要去更新这行数据的,事务A是要去读取这行数据的值的。
现在事务A直接开启一个ReadView,这个ReadView 里的m_ids 就包含了事务A 和 事务B 的两个id, 45 和 59,然后min_trx_id 就是45,max_trx_id 就是60, creator_trx_id 就是 45,是事务A自己。
这个时候事务A第一次查询这行数据,会走一个判断,就是判断一下当前这行数据的 txr_id 是否小于 ReadView 中的 min_trx_id,此时发现 txr_id=32,是小于ReadView 里的 min_trx_id 45的,说明你事务开启之前,修改这行数据的事务早就提交了,所以此时可以查询这行数据。
接着事务B开始动手,他把这行数据的值修改为值B,然后这行数据的 txr_id 设置为自己的id, 也就是59,同时roll_pointer 指向了修改之前生成的一个 undo log,接着这个事务B就提交了。
这个时候事务A再次查询,此时查询的时候,会发现一个问题,那就是此时数据行的里的 txr_id=59,那么这个 txr_id 是大于 ReadView 里的 min_txr_id(45),同时小于 ReadView 里的 max_trx_id(60)的,说明更新这条数据的事务,很可能就跟自己差不多同时开启的,查看在 m_ids 列表里。所以这行数据不能查询。他会顺着这条数据的 roll_pointer 找到最近的一条undo_log,trx_id是32。 接着假设事务A 更新了这行数据的值,改为A,trx_id 修改为45,同时保存之前的事务B修改值的快照。此时事务A来查询这条数据的值,会发现这个 trx_id=45,跟自己的 ReadView 里的 creator_trx_id(45)是一样的。说明这个值就是自己修改的,可以读到。
接着在事务A执行过程中,突然开启了一个事务C,这个事务的id 是78,然后更新了那行数据的值为C,还提交了。这个时候事务A再去查询,会发现当前数据的 trx_id=78,大于了自己的 ReadView 中的 max_trx_id(60),此时说明这个事务是在自己开启之后,然后才更新的,不能看到。然后会顺着往下找到自己能看到的。
这套机制就保证了你只能读到事务开启之前的,别的提交事务更新的值,还有就是你自己事务更新的值。假如说你事务开启之前,就有别的事务真正运行,然后你事务开启之后,别的事务更新了值,你是绝对读不到的。或者你事务开启之后,比你晚开启的事务更新了值,你也是读不到的。
3. 基于ReadView 机制实现 Read Committed 隔离级别
RC隔离级别就是说事务运行期间,只要别的事务修改数据还提交了,你可以读到人家修改的数据的,所以是会发生不可重复读和幻读的问题。
ReadView 机制实现RC机制一个非常核心的要点就是,每次发起查询的时候,都需要重新生成一个ReadView。
举个例子:
假设数据库当前有一行数据,是事务id=50 的一个事务之前就插入进去的,然后现在,活跃着两个事务,一个是事务A(id=60),一个是事务B(id=70)。
此时事务B发起了一次update 操作,更新了这条数据,把这条数据的值修改为值B,所以此时数据的trx_id 会变为事务B 的id=70,同时会生成一条 undo log,由roll_pointer 来指向。
如果此时,事务A要发起一次查询操作,会生成一个ReadView,此时ReadView 里的 min_trx_id=60, max_trx=71,creator_trx_id=60。它发现当前这条数据的 trx_id 是70, 也就是说,属于ReadView 的事务id 范围之内,说明是他生成 ReadView 之前就有这条活跃的事务,是这个事务修改了这条数据的值,但是此时这个事务B还没提交,所以ReadView 的 m_ids 活跃事务列表里,是有[60,70]两个id的,所以此时根据ReadView 的机制,此时事务A是无法查到事务B修改的值B的。
接着就顺着undo log 版本链条往下查找,就会找到一个原始值,发现他的 trx_id 是50,小于当前ReadView 里的 min_trx_id,说明他生成的ReadView 之前就有一个事务插入这个值并早就提交了。因此可以查到这个原始值。
接着,假设事务B此时提交了,那么按照RC的隔离级别定义,事务B此时一旦提交了,说明事务A下次查询的时候,就可以读到事务B修改过的值了。因为事务B已经提交了。
那么怎么让事务A能够读到事务B修改后的值呢?
很简单,就是让事务A下次发起查询,再次生成一个ReadView,此时再次生成ReadView,数据库内活跃的事务只能事务A了,因此min_trx_id,mac_trx_id 是71,但是 m_ids 这个活跃事务列表里,只会有一个60了,事务B的id=70不会出现在 m_ids 活跃事务列表里。
此时事务A再次基于这个ReadView 去查询,会发现这条数据的 trx_id=70,虽然在ReadView 的 min_trx_id 和 max_trx_id 之间,但是此时并不在m_ids 列表内,说明事务B在生成本次 ReadView 之前就已经提交了。那么事务A就可以读到事务B。
4. 基于ReadView 机制实现 Replated Red 隔离级别
在RR级别下,你的事务无论读多少次,都是一个值,别的事务修改数据之后哪怕提交了,你也看不到,这就避免了不可重复读的问题。同时如果别的事务插入一些新的数据,你也看不到,这样就避免了幻读的问题。
举个列子看看如何解决:
首先加入有一条数据是事务id=50的一个事务插入的,同时此时有事务A和事务B同时运行,事务A的id 是 60,事务B的id 是70。
这个时候,事务A发起了一个查询,会生成一个ReadView,此时ReadView 里的 creator_trx_id是60,min_trx_id是60,Max_trx_id 是71,m_ids是[60,70]。当A基于这个ReadView 去查这条数据,会发现这条数据的 trx_id 为50,是小于 ReadView 里的 min_trx_id 的,说明他发起查询之前,早就有事务插入这条数据还提交了,所以可以查到该条原始值。
接着就是事务B此时更新了这条数据的值为值B,此时会修改trx_id为70,同时生成一个 undo log,而且关键的事务B此时他还提交了,事务B结束。
此时ReadView 中的 m_ids 还是 60 和 70 么,显然是的,因为ReadView 一旦生成了就不会改变了。此时,事务A去查询这条数据的值,它会惊讶的发现此时数据的 trx_id 是70了,70 一方面是在 ReadView 的 min_trx_id 和 max_trx_id 的范围区间的,同时还在m_ids 列表中。
说明起码是事务A开启查询的时候,id 为70 的这个事务B还在运行的,然后由这个事务B更新了这条数据,此时事务A是不能查询到事务B更新的这个值的,因此这个时候继续顺着指针往历史版本链条上去找。
接着事务A顺着指针找到下面一条数据,trx_id 为50,是小于 ReadView 的 min_trx_id 的,说明在他开启查询之前,就已经提交了,所以能查到。
这就解决了不可重复读的问题。
下面看看如何解决幻读的问题。假设现在事务A先用 select * from x where id > 10 来查询,此时可能查询到的就是一条数据,而且读到的是原始值的那个版本。
接着事务A再次查询,此时会发现符合条件的有2条数据,一条是原始值那个数据,一条是事务C插入的那条数据,但是事务C插入的那条数据的 trx_id 是80 ,这个80 是大于自己的 ReadView 的 max_trx_id 的,说明是自己发起查询之后,这个事务才启动的,所以此时这条数据是不能查询的。
因此事务A再次查询,还是只能查询到原始值这一条数据。解决了幻读问题。