mysql mvcc 机制

1、前言

mvcc 即多版本并发控制,即通过多版本的方式实现读写数据的高并发,主要是通过多版本和锁来实现的。多版本是使用版本链 + undo log,锁是使用间隙锁。

版本链是怎么实现的呢?
在 innerdb 数据的每一行,出来存储的数据之外,还存储了隐藏的两列,分别为 trx_id和db_roll_ptr。trx_id表示最近修改的事务的 id(即当前事务启动时的分配的事务 id),db_roll_ptr 指向 undo segment 中的 undo log(即指向老版本),因为每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志中。我们知道,undo log 主要记录了数据之前的数据信息,通过这些信息可以还原到之前版本的状态,这玩意在事务中非常有用,而且在 mvcc 中,我们也要用。

一个版本链类似于这样:


版本链

那么对于可重复读以及读已提交,我们怎么通过上面所说的东西是实现事务呢?这里就要引入 readview 了,这个 ReadView 中主要包含4个比较重要的内容:

  • m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的 事务id 列表。
  • min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是 m_ids 中的最小值。
  • max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值(注意:max_trx_id 并不是 m_ids 中的最大值,事务 id 是递增分配的。比方说现在有id为1,2,3这三个事务,之后 id 为3的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括1和2,min_trx_id 的值就是1,max_trx_id 的值就是4。
  • creator_trx_id :表示生成该 ReadView 的事务的 事务id (只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0)

对于查询时的版本链数据是否看见的判断逻辑:

  • 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

2、例子

举一个例子,比如我们有一个表 person,属性为 (id, name),刚开始有一条数据(它是事务10创建的),数据格式为:


事务10

然后有一个事务11,它更新了这条数据,将名字变成 LiLi,然后事务提交,那么版本链如下所示:


事务11

在事务11后,又有一个事务12,它也更新了这条数据,将名字变成了 Joke,但是没有提交,版本链如下所示:


事务12

读以提交的情况下:
假设在事务11更新后事务12更新前 select(读取不分配事务id,所以你 select 不会有新的版本数据),那么对于 select 来说,min_trx_id = 12,max_trx_id = 12 + 1 = 13,m_ids = [12],m那么根据上述规则,从版本链的最新版本开始读,发现 12 在 m_ids 中,则往下一个。然后一直找发现 11 < 12,则说明 select 此时只能读到版本为11的数据,即名字为 LiLi。

然后事务13又到12更新后并提交事务后再 select,此时 m_ids = [],根据规则可以读到版本12,即 Joke。

可重复读的情况下:
事务的 readview 是在事务开始的时候生成的,所以分析更为简单,事务开始的时候分析哪些活跃的事务,版本链为什么,后续 readview 都使用这个。比如事务按照上述顺序,select 开始读到的版本为10,如果 select 事务没结束,就算事务13提交了,读到的数据都是10不变。

3、update

对于 select 来说,是版本读。那么对于 update 来说,却是当前读,即 update 的时候总是读当前最新版本的数据然后再进行更新,所以经常会有可重复读的情况下,update 冲突的情况。

4、后记

1、事务不是在 begin transition 就开始的,而是在第一个 sql 语句开始的。
2、在可重复读的情况下,readview 实在事务开始的时候(结合1)创建的;而在读已提交的情况下,readview 是在每次 select 查询的时候重新生成的。
3、在 innerdb 事务中,行锁是在更新行的时候加上,但并不是马上释放,而是等事务结束的时候才释放,这就是两阶段锁协议。
4、至于 mvcc 能解决幻读问题,为啥还用锁(快照读用 mvcc,当前读用间隙锁),看这篇回答:https://www.zhihu.com/question/372905832

你可能感兴趣的:(mysql mvcc 机制)