Posts tagged ‘mvcc’

MVCC read view的问题

之前写了一篇文章以为对MVCC的大致原理有些了解了。今天看了《高性能MySQL》的时候,深究了一下read view的问题,发现还是蛮有意思的。

 

特别画了一张图来确认一下。

本文是上一篇MySQL事务和MVCC简介的后续,建议先了解上一篇文章以后再阅读本文。

上一篇文章简单描述了MVCC的相关情况,但是没有详细说,read view是什么结构,并且它到底是怎么工作的。

比如,我们在show engine innodb status可以看到如下内容:

  1.   —TRANSACTION 0 600, ACTIVE 4 sec, process no 3396, OS thread id 1148250464, thread declared inside InnoDB 442
  2.   mysql tables in use 1, locked 0
  3.   MySQL thread id 8079, query id 728899 localhost baron Sending data
  4.   select sql_calc_found_rows * from b limit 5
  5.   Trx read view will not see trx with id>= 0 601, sees <0 596

要理解这个,我们首先要知道:

read view其实就是一个保存事务ID的list列表。记录的是本事务执行时,MySQL还有哪些事务在执行。

Read Repeatable(下文和图中用RR表示)对应的是在每个事务启动的时候创建 一个Read View。

Read Commit(下文和图中用RC表示)对应的是每次执行SQL statement时候创建 一个Read View。

 

根据show engine innodb status的输出是说看到这个事务的id是600。

对这个事务来说,trx id为596以下的所有事务修改的行数据,这个事务都可以看到,

trx id在601以上的事务修改的数据,这个事务都不应该读取到。

596到601号事务,一共5个事务修改的数据无法确定是否能够读取。read view应该为这5个事务id集合的子集。

 

如果线程的隔离级别是RR:

按照show engine innodb status的输出,600号事务在事务启动的时候,MySQL告诉它:

596之前的所有事务都已经提交了(Trx read view will not see trx with id>= 0 601, sees <0 596),

由于事务本身是600号,那么对应的601号事务因为是在它后面启动的,600号事务肯定无法提供读取到数据(Trx read view will not see trx with id>= 0 601, sees <0 596)。

read view表示的是事务开始时MySQL还有哪些事务在执行,就应该为{596,597,598,599}集合的子集,假设为{596,598},

根据read view,Innodb在读取数据的时候需要判断该行数据的修改事务号,判断的方法为:

a) 如果行数据的修改事务号小于596,由于在事务启动的时候596之前的所有线程都已经提交了,那么该行数据可读。

b) 如果行数据的修改事务号大于601,那么该行数据肯定不可读。如果事务号为600(即自己),本事务未提交,当然也是不可读的。

为了保证在事务内任何时间读取的数据都是一致的,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

c) 如果行数据的修改事务号在read view里面{596,599},说明是该事务(600号)开始时没有提交的数据修改,

为了保证在事务内任何时间读取的数据都是一致的,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

d)如果不在read view里面,即事务id号在{597,598}中,说明修改行数据是该事务(600号)开始时已经提交的数据修改,那么该行数据可读。

mvcc_readview

图1 MySQL read view 示意图

如图1。这个事务的行修改数据在{[0~595],597,598}是可读区间,{596,599,600,[601~ +infinity]}是不可读区间。

 

 

如果线程的隔离级别是RC,线程开始的时候,RC事务并不会做read view,此时开始的SQL跟上面RR的情况可能是一样的。

但是过了一段时间如果601事务提交了,同样的查询,在RC下面提交,对应的show engine innodb status的信息可能稍微有点不同:

  1.   —TRANSACTION 0 600, ACTIVE 4 sec, process no 3396, OS thread id 1148250464, thread declared inside InnoDB 442
  2.   mysql tables in use 1, locked 0
  3.   MySQL thread id 8079, query id 728899 localhost baron Sending data
  4.   select sql_calc_found_rows * from b limit 5
  5.   Trx read view will not see trx with id>= 0 602, sees <0 596

按照输出,600号事务在语句“select sql_calc_found_rows * from b limit 5”发起的时候,MySQL告诉它:

596之前的所有事务都已经提交了(Trx read view will not see trx with id>= 0 601, sees <0 596),

对应的,602号线程以及它之后的所有线程都还未提交(Trx read view will not see trx with id>= 0 602, sees <0 596)。

read view表示的是语句开始时MySQL还有哪些事务在执行(注意,这里跟RR为事务开始的时候的read view不同了),

在一个事务里面,每个SQL执行的时候,它的read view都可能是不同的。有可能事务启动的时候的sql的read view为{596,598},

这个语句执行的时候,601事务提交了,read view为{596,598}。

注意,601号事务虽然在600事务后启动,此时已经提交了行数据修改,它修改的数据,600号线程也可以读到。

根据read view,InnoDB在读取数据的时候需要判断该行数据的修改事务号,判断的方法为:

a) 如果行数据的修改事务号小于596,由于在语句启动的时候596之前的所有线程都已经提交了,那么该行数据可读。

b) 如果行数据的修改事务号大于等于602,那么该行数据肯定不可读。如果事务号为600(即自己),本事务未提交,当然也是不可读的。

为了保证读到的是Commited的数据,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

c) 如果行数据的修改事务号在read view里面{596,599},说明是该语句开始时没有提交的数据修改,

为了保证读到的是Commited的数据,需要根据行数据的undo信息回溯,每次回溯都需要进行a),b),c),d)的判断,直到找到一个可读的数据。

d)如果不在read view里面,即事务id号在{597,598}中,说明修改行数据是该语句开始时已经提交的数据修改,那么该行数据可读。

 

如图1。这个语句的修改行数据的事务id在{[0~595],597,598,601}是可读区间,{596,599,600,[602~ +infinity]}是不可读区间。

 

整体来说,这篇文章描述了在Read Readrepeatable和Read Commit环境下,MySQL根据Read View读取数据的方法,来保证可重复读和只读到已经提交的数据。

MySQL事务和MVCC简介-update流程

最近在学习和研究MySQL的MVCC和事务相关机制。
简单整理一下,希望够通俗易懂了啊。
不过读本文之前,推荐先阅读

  • 《MySQL数据库InnoDB存储引擎Log漫游》 三篇系列文章:http://www.mysqlops.com/2012/04/06/innodb-log1.html

这三篇文章通俗易懂的介绍了MySQL本身undo和redo的结构和相关概念。

  • 《MySQL Innodb日志机制深入分析》http://blog.csdn.net/yah99_wolf/article/details/6567869

这篇文章主要介绍了check_point,lsn等相关redo文件位置的概念。

 

如果你对b-tree等MySQL基础的索引结构不了解的话,请详细阅读:

  • 《由浅入深理解索引的实现》 http://www.mysqlops.com/2011/11/24/understanding_index.html

另外,说一句,上面的两个系列文章都是sleebin9的作品,这家伙技术牛逼,文字功底和逻辑思维能力也太牛了,非池中物啊!

 

  • 如果想了解MySQL文件io相关的资料和参数,请参考淘宝丁奇的ppt:http://www.slideshare.net/mryufeng/mysqlio-12891332

文章中有任何错漏之处,麻烦各位大牛及时指出。

 

 

update流程事务相关介绍

假设现在有一个事务,需要执行以下的一条update语句。
那么在MySQL上,它是怎么完成的列。
begin
update test set c1=1 where id =5
commit
注:原c1=3

本文主要关注undo,redo等涉及事务信息,对细节和锁的详情不详细深入。
锁的情况可以参考:《MySQL数据库InnoDB存储引擎中的锁机制》http://www.mysqlops.com/2012/05/19/locks_in_innodb.html#more-3169

 

 

begin:
根据事务隔离级别,可能要记录read view等信息。

update 语句:
1、走btree找到id=5的叶子节点。这里走到页一级以后,该页需要加入buffer pool,并根据访问频率调整它在LRU list中的位置。
2、由于是update,他需要更新该行记录。所以需要获得X锁。注意,我们这里修改的只是非主键。如果修改主键值,(比如 update test set id=1 where id=5)那么MySQL对应的会在新主键页上插入一行(id=1),并将原主键行(id=5)处标记为已删除。如果新增的行导致页分裂,那么Innodb在现有的版本上还需要锁定上层的中间节点。目前Innodb在所有中间节点上只有一个锁,如果还有其他节点需要分裂,那就只能等待这个唯一的锁了。
3、获得undo slot。在目前的版本中,只有一个undo segment,每个undo segment包含有效的undo slot只有1023个(新版本中应该有128*1023个)。每个线程可能有一个或者两个undo slot。insert undo slot(insert语句需要申请)或者update undo slot(delete和update申请update undo slot)。所以并发的写线程太多,在MySQL来说目前还是有问题的,MySQL有可能会报undo slot槽不够了。每个undo slot对应一个线程,用完了需要归还的。为了实现MVCC,MySQL使用了review。这里还有一个read view的问题。MySQL支持四种事务隔离级别 Serial 本文后面简称SER, Read Repeatable 本文后面简称RR, Read Commit 本文后面简称RC, Read Uncommit 本文后面简称RU。read view是在每个事务或者query开始的时候保存起来的当前事务最小和最大事务号,根据会话的事务隔离级别不同而在不同层次上保存。在走到页节点查询数据的时候可以用这两个事务号来对比,以确定对应数据对本事务是否可见。例如,在RC下是为每个Query保存了一个read view,这样其他的事务提交了,本事务在query开始前保存的read review就大于已提交的事务,可以读已提交事务的数据。而在RR下,read view是在事务开始前保存的,那么read view事务号就小于那些在本事务开始后提交的事务,也就无法看到在本事务开始后其他事务提交的数据了,保证了可重复读。
4、按照日志先行的策略,需要把日志写进去才能做具体的操作,那么这里我们要写undo,也需要先把undo 的redo写好。所以MySQL会先在redo中记录undo的space_id,page_id,trx_id。redo为物理页记录方式。为crash恢复做准备。
5、在undo中记录c1=3。undo是用逻辑方式记录的。undo slot记录了c1=3的信息。
6、日志先行,我们现在需要修改具体的数据了,先记录redo。c1=1的物理页
7、真正的修改id=5这一行。对应行的undo ptr指向c1=3的undo slot。修改了数据以后,该页为脏页,由mysql的master thread来

commit:
8、写binlog buffer,根据sync_binlog来判断是否要刷盘。
9、确认是否能够释放undo slot。如果是insert 的话,undo slot可以立即释放,因为它的undo 信息不被其他线程所需要(其他线程不会去访问一个不存在的数据)。否则不能清理的话,转到undo history里面去。
10、根据innodb_flush_log_at_trx_commit的设置判读是否需要刷buffer和刷盘。
11、在iblog中记录事务提交信息。这个信息只保存在redo log的第一个文件头上。返回用户事务执行成功。

 

 

 

 

crash recovery:
crash recovery主要是基于check point来使数据达到一致性状态的。
对应的在前面介绍的场景下恢复分为以下几种情况:
这里有几个关键节点:
a、第4步:undo的redo写入磁盘。
b、第6步:数据变更的redo写入磁盘。
c、checkpoint大于第6步写入磁盘的lsn。

 

  • 在第4步以前,如果crash掉了。那么MySQL重启以后,读redo也无法得到任何改事务的操作信息。不过幸运的是,数据库也没有返回给应用程序说操作成功。应用程序进行异常处理就行了。
  • 如果b之前crash,由于undo的信息写入了redo里面,MySQL读取redo以后,会构建undo信息。但是此时checkpoint肯定还没有赶上(参考《MySQL Innodb日志机制深入分析》)。此时数据没有被修改,通过检查页上最新的修改trx_id就可以判断回滚策略了。这里也就是说数据不用修改。由于MySQL也没有返回给用户成功的信息,所以也没有问题。
  • 下面就要分两种情况了。第一种情况:如果应用程序收到了操作成功的信息,但是checkpoint没有赶上。crash恢复的时候,在checkpoint之后提交的事务全部要回滚,redo恢复的时候不是把undo信息恢复出来了吗,根据undo信息和页上最新修改trx_id,就可以将数据页恢复到c1=3。这里应用程序收到了update成功的消息,但是实际上数据却恢复为c1=3,就相当于数据丢失了。另外,因为binlog此时有可能已经生成并传给备机(这里有可能在本地还没有落到磁盘,还在buffer里面,就通过网络上传输到备机了,我就遇到过很多次),那么备机有可能已经接收并应用成功了。这种情况下主备机的数据都不一致了。
  • 另外一种情况,checkpoint赶上来了。那么皆大欢喜,redo恢复完了以后,这个undo可以直接丢掉。并且数据保持为c1=1

 

 

 

MVCC多版本介绍

上面是undo,redo和crash recovery的一些内容。

跟MVCC关系不大,我们再简单说说此时有并发读的情况
如果在此时有select * from test where id=5的读过来。
根据事务隔离级别有SER,RR,RC,RU几种区别。在各个隔离级别下有不同的read view。
注意:MySQL的每一行都记录了最新操作它的trx_id

 

  • 序列SER:由于没有并发,所以读到的数据肯定是之前commit好的,放心读吧。

其他的情况下:MySQL首先会判读read view。

  • 可重复读RR:小于read view最小的trx_id的,这一行已经是事务开始之前commit的,那么可以放心大胆的读。大于read view最小的trx_id,为了保证可重复读,那么就需要根据undo ptr找到最新但是trx_id小于本事务trx_id的版本。Read view是在事务开始的时候建立的。
  • 读提交过的数据RC:跟RR不同,read view是query开始的时候建立的。小于read view最小的trx_id的,这一行已经是事务开始之前commit的,那么可以放心大胆的读。大于read view最小的trx_id,为了保证读已提交的,只需要找到最新的已经提交的版本(如果有trx操作了它,但是在做其他的update之类的,那么这个版本就需要忽略,继续回溯)
  • 读未提交的数据RU:看到什么读什么,管你有没有提交。