1.undo log版本链
MySQL中的每条数据都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer指向更新这个事务之前生成的undo log。
例:假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示,插入这条数据的值是值A,因为事务A的id是50,所以trx_id就是50,roll_pointer指向一个空的undo log,因为之前没有这条数据。
接着现在有个事务B来修改这条数据,把值改成了值B,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示。
2.基于undo log版本链实现的ReadView机制
每执行一个事务的时候,就会生成一个ReadView,里面主要包含:
- m_ids:没有提交的事务id;
- nin_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,说明事务开启之前,修改这行数据的事务早已提交,此时可以查到这行数据,如下图所示。
接着事务B要把这行数据的值改为值B,然后这行数据的txr_id设置为自己的id,同时roll_pointer指向了修改之前生成的一个undo_log,然后事务B提交了,如下图所示。
这时事务A再次查询,会发现此时数据行里的txr_id=59,大于ReadView里的min_txr_id(45),同时小于ReadView里的max_trx_id(60),说明更新这条数据的事务,是跟自己差不多同时开启的,于是会查看这个txr_id=59,是否在ReadView的m_ids列表里。此时,在ReadView的m_ids列表里,有45和59两个事务id,就证实这个修改数据的事务是跟自己并发执行然后提交的,所以对这行数据不能直接查询,如下图所示。
接着,事务A会顺着这条数据的roll_pointer指向的undo_log日志链条往下找,就会找到最近一条undo_log,trx_id是32,此时发现trx_id是小于ReadView里的min_trx_id(45)的,说明这个undo log版本是在事务A开启之前就执行且提交的,如下图所示。
如果在undo_log版本链中读到了跟自己相等的trx_id,那么证明这条数据是自己修改的,是可以读取的;如果读取的trx_id大于了ReadView中的creator_trx_id,那么说明在自己开始事务之后有事务修改了值,那么也不能直接读取数据。
3.基于ReadView机制实现Read Commintted
当一个事务设置为RC(Read Commintted)隔离级别的时候,每次发起查询,都会重新生成一个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_id=71,creator_trx_id=60,如下图所示。
此时,事务A发现这条数据的trx_id是70,属于当前ReadView的事务id范围内,根据ReadView机制,事务A是无法查到事务B修改的值B的。
接着事务A就顺着undo log版本链往下查找,就会找到一个原始值,发现它的trx_id是50,小于当前ReadView里的min_trx_id,说明它在生成ReadView之前,就有一个事务插入了这个值并且早就提交了,因此可以查到这个原始值,如下图。
接着,事务B 此时提交了,那么事务B就不活跃于数据库里了,那么,当事务A再次查询时,会再次生成一个ReadView,因为数据库内活跃的只有事务A,因此min_trx_id是60,max_trx_id是71,但是m_ids只有一个60,如下图。
事务A再次查询,会发现这条数据的trx_id=70,虽然在ReadView的min_trx_id合max_trx_id范围之间,但是不在m_ids列表内,说明事务B在生成本次ReadView之前就已经提交了,那么事务A就可以查到值B,如下图。
每次查询都会生成新的ReadView,如果在这次查询之前,有事务修改了数据并且提交了,那么这次生成的ReadView的m_ids列表就不包含已提交的事务,那么就可以查询到被修改后的值。
4.基于ReadView机制实现Repeatable Read
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],此时ReadView如下图所示。
事务A基于这个ReadView去查询这条数据,会发现这条数据trx_id=50,是小于ReadView中的min_trx_id,说明在发起查询之前,就有事务插入了这条数据并且提交了,所以此时可以查询到这条原始值。
此时事务B更新这条数据的值为值B,trx_id修改为70,同时生成一个undo log,并且事务B提交了。
事务B虽然结束了,但是事务A的ReadView里,还是会有60和70两个事务id。
此时事务A再去查询这条数据,会发现此时这条数据的trx_id是70,说明事务A开启查询的时候,id为70的事务B在运行,然后事务B修改了这条数据,此时事务A是不能查询到事务B更新的这个值的,事务A会顺着版本链往下找,会找到指针下面的一条数据,trx_id为50,小于ReadView的min_trx_id,说明在开启查询之前,就提交了这个事务,所以事务A可以查到这个值。
5.Repeatable Read如何解决幻读
新事物插入数据的事务id都会大于之前事务ReadView中的min_trx_id,所以之前事务再次读取数据,不会读到新事物插入的数据。
6.MVCC机制
MVCC(multi-version concurrent control)机制,就是多版本并发控制机制。
MySQL实现MVCC机制,是基于undo log多版本链条+ReadView机制来做的。
