深入浅出INNODB MVCC机制与原理

https://blog.csdn.net/youling_lh/article/details/10473679

摘要:

1、基础知识

2、MVCC实现原理以及视图化理解(包含些测试以便理解)

3、深MVCC实现机制

一、基础知识

事务:

事务是一组原子性sql查询语句,被当作一个工作单元。若mysql对改事务单元内的所有sql语句都正常的执行完,则事务操作视为成功,所有的sql语句才对数据生效,若sql中任意不能执行或出错则事务操作失败,所有对数据的操作则无效(通过回滚恢复数据)。事务有四个属性:

1、原子性:事务被认为不可分的一个工作单元,要么全部正常执行,要么全部不执行。

2、一致性:事务操作对数据库总是从一种一致性的状态转换成另外一种一致性状态。

3、隔离性:一个事务的操作结果在内部一致,可见,而对除自己以外的事务是不可见的。

4、永久性:事务在未提交前数据一般情况下可以回滚恢复数据,一旦提交(commit)数据的改变则变成永久(当然用update肯定还能修改)。

ps:MYSAM 引擎的数据库不支持事务,所以事务最好不要对混合引擎(如INNODB 、MYISAM)操作,若能正常运行且是你想要的最好,否则事务中对非支持事务表的操作是不能回滚恢复的。

读锁:

也叫共享锁、S锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S 锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

写锁:

又称排他锁、X锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

表锁:操作对象是数据表。Mysql大多数锁策略都支持(常见mysql innodb),是系统开销最低但并发性最低的一个锁策略。事务t对整个表加读锁,则其他事务可读不可写,若加写锁,则其他事务增删改都不行。

行级锁:操作对象是数据表中的一行。是MVCC技术用的比较多的,但在MYISAM用不了,行级锁用mysql的储存引擎实现而不是mysql服务器。但行级锁对系统开销较大,处理高并发较好。

MVCC:多版本并发控制(MVCC,Multiversion Currency Control)。一般情况下,事务性储存引擎不是只使用表锁,行加锁的处理数据,而是结合了MVCC机制,以处理更多的并发问题。Mvcc处理高并发能力最强,但系统开销比最大(较表锁、行级锁),这是最求高并发付出的代价。

Autocommit:是mysql一个系统变量,默认情况下autocommit=1表示mysql把没一条sql语句自动的提交,而不用commit语句。所以,当要开启事务操作时,要把autocommit设为0,可以通过“set session autocommit=0; ”来设置

二、MVCC实现原理以及例化理解(包含些测试以便理解)

第一:先看看网络上几乎全部一样的理解,包括《高性能mysql第二版(中文版)》也如此说明,这样是很容易理解。但笔者觉得2个地方不妥,先看内容,在后面笔者会给出不妥地方用(1、2…)加粗标志出来,且给出测试证明。

Ps:这些只是外部看来的理解层面,深层次在第三点讲解

------------------------------------------

InnoDB实现MVCC的方法是,它存储了每一行的两个(1)额外的隐藏字段,这两个隐藏字段分别记录了行的创建的时间和删除的时间。在每个事件发生的时候,每行存储版本号,而不是存储事件实际发生的时间。每次事物的开始这个版本号都会增加。自记录时间开始,每个事物都会保存记录的系统版本号。依照事物的 版本来检查每行的版本号。在事物隔离级别为可重复读的情况下,来看看怎样应用它。

SELECT

Innodb检查没行数据,确保他们符合两个标准:

     1、InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行

     2、行的删除操作的版本一定是未定义的或者大于当前事务的版本号。确定了当前事务开始之前,行没有被删除(2)

  符合了以上两点则返回查询结果。

  INSERT

     InnoDB为每个新增行记录当前系统版本号作为创建ID。

  DELETE

     InnoDB为每个删除行的记录当前系统版本号作为行的删除ID。

  UPDATE

InnoDB复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。

----------------------------------------------

(1)    不是两个,是三个。

1DB_TRX_ID:一个6byte的标识,每处理一个事务,其值自动+1,上述说到的“创建时间”和“删除时间”记录的就是这个DB_TRX_ID的值,如insert、update、delete操作时,删除操作用1个bit表示。 DB_TRX_ID是最重要的一个,可以通过语句“show engine innodb status”来查找,如下:

   -----------------------------------------

   ……

      TRANSACTIONS

------------

Trx id counter 0 430621

Purge done for trx's n:o < 0 430136 undo n:o < 0 0

History list length 7

……

   ------------------------------------------

2DB_ROLL_PTR: 大小是7byte,指向写到rollback segment(回滚段)的一条undo log记录(update操作的话,记录update前的ROW值)

3DB_ROW_ID: 大小是6byte,该值随新行插入单调增加,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,不然的话聚集索引中不包括这个值. 这个用于索引当中

(2)    这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候。网上的说法很容易让读者误解

(3)    这点上面没有标注,在insert操作时 “创建时间”=DB_ROW_ID,这时,“删除时间 ”是未定义的;在update时,复制新增行的“创建时间”=DB_ROW_ID,删除时间未定义,旧数据行“创建时间”不变,删除时间=该事务的DB_ROW_ID;delete操作,相应数据行的“创建时间”不变,删除时间=该事务的DB_ROW_ID;select操作对两者都不修改,只读相应的数据

第二、下面用图形化形式表示MVCC如何处理select、insert、delete、update

有两个事务A、B

假设开始时间顺序ABCD,且DB_TRX_ID满足以下情况

A. DB_TRX_ID = 2010

B. DB_TRX_ID = 2011

C. DB_TRX_ID = 2012

D. DB_TRX_ID = 2013

注意:

1、B. DB_TRX_ID> A. DB_TRX_ID是因为DB_TRX_ID的值是系统版本号的值,系统版本号是自动增加的,所以DB_TRX_ID也是自动增加。但是会出现这种情况,假如A事务开始后B事务开始前有一个insert操作插入一行数据(没有bengin、comint),则B. DB_TRX_ID= A. DB_TRX_ID+1+1 ,并不符合不是说系统版本号增量为1,其实并不矛盾,其实每一条sql操作可以当作一个事务,因为autocommit=1,所以这个insert操作是一个事务,A事务之后新增2个事务, 所以是加2而不是1。

2、下面例化图只是笔者方便大家理解而设计的图片,红色框代表隐藏两列

例化1:SECLET

这是表test数据

trx代表改行数据是那个事务创建

creat_num是“创建时间”,也就是DB_TRX_ID值

dele_num是“删除时间 ”,空列代表没被任何事务标志为已“删除”,图中id为2的数据行的dele_num=2012表示事务C“删除”了改行。


 

B事务有select * from test;语句,按照MVCC原理,该语句相当于:select * from test where creat_num>=2011 and (dele_num=NULL OR dele_num>2011),所以返回数据是id为1、2行。

D事务select * from test;则返回出id为2的行。因为2行被C事务删除了。

例化2:UPDATE


 

A事务一条语句“update from test set col=’winben’ where col=’benwin’”。

则先复制一条数据如蓝色框,creat_num=DB_TRX_ID(这里是2010),dele_num=NULL,然后把旧行数据的设dele_num=2010,等commit后则删除旧数据行

例化3:DELET

删除就是设dele_num= DB_TRX_ID

-------于2012.12.23加上start----------------------------------------------------------------

和一位淘宝网友讨论一个问题(关于事务隔离级别,这里就直接贴不整理了)

网友: 请教个问题,innodb的事务,一定是按ID顺序提交么? ID为101的一定在ID为100的事务之后?

笔者:这个问题我也不确定。我认为不是按顺序的,可以这样想一下,加入a事务很大是id100,然后还没commit之前有id为101的事务b并发开始处理,但b事务很小处理完了,如果要等a事务的话则是一个鸡肋了。当然还有考虑锁的问题,如果a事务设置了排他锁,且b事务有写操作那不事务则在等待队列中了,那commit的顺序肯定是a然后b的!

网友:如果是这样的话,假设有100,101,102三个事务,101最先提交了,这时新事务103,应该能看到101的更改,而如果按当前活跃ID的最小的比较(这时为100),那就看不到101的更新。

笔者:结合事务隔离级别:

1、READ UNCOMMITTED ,不适用MVCC读,可以读到其他事务修改甚至未提交的

2、READ COMMITTED ,其他事务对数据库的修改,只要已经提交,其修改的结果就是可见的,与这两个事务开始的先后顺序无关,不完全适用于MVCC读,

像你说的100的读101的是可以的(按照MVCC理论应该不行的),但适用102读101(能套MVCC理论)。

3、REPEATABLE READ,可重复读,完全适用MVCC,只能读取在它开始之前已经提交的事务对数据库的修改,在它开始以后,所有其他事务对数据库的修改对它来说均不可见

4、 SERIALIZABLE ,完全不适合适用MVCC,这样所有的query都会加锁,再它之后的事务都要等待

MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下

 

-------于2012.12.23加上start----------------------------------------------

三、深入MVCC实现机制

1、到这里很多人就会发现,如果确实根据creat_num 即时事务DB_TRX_ID去比较获取事务的话,按道理在一个事务B(比A后,但A还没commit)select的话B. DB_TRX_ID>A.DB_TRX_ID则应该能返回A事务对数据的操作以及修改。那不是和前面矛盾?其实不然,只是前面没有讲到以下内容。

InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),然后一致性读去比较记录的tx id的时候,并不是根据当前事务的tx id,而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。


 

在storage/innobase/read/read0read.c中实现了创建read view的函数read_view_open_now,在storage/innobase/include/read0read.ic中实现了判断一致性读是否可见的read_view_sees_trx_id

代码:

  1. read_view_t*  
  2. read_view_open_now(  
  3. /*===============*/  
  4.          trx_id_t    cr_trx_id,          /*!< in: trx_id of creating 
  5.                                               transaction, or 0 used in purge */  
  6.          mem_heap_t*          heap)                 /*!< in: memory heap from which 
  7.                                                allocated */  
  8. {  
  9.          read_view_t*  view;  
  10.          trx_t*                trx;  
  11.          ulint          n;  
  12.          ut_ad(mutex_own(&kernel_mutex));  
  13.          view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);  
  14.          view->creator_trx_id = cr_trx_id;  
  15.          view->type = VIEW_NORMAL;  
  16.          view->undo_no = 0;  
  17.          /* No future transactions should be visible in the view */  
  18.          view->low_limit_no = trx_sys->max_trx_id;  
  19.          view->low_limit_id = view->low_limit_no;  
  20.          n = 0;  
  21.          trx = UT_LIST_GET_FIRST(trx_sys->trx_list);  
  22.          /* No active transaction should be visible, except cr_trx */  
  23.          while (trx) {  
  24.                    if (trx->id != cr_trx_id  
  25.                        && (trx->conc_state == TRX_ACTIVE  
  26.                             || trx->conc_state == TRX_PREPARED)) {  
  27.                             read_view_set_nth_trx_id(view, n, trx->id);  
  28.                            n++;  
  29.                             /* NOTE that a transaction whose trx number is < 
  30.                             trx_sys->max_trx_id can still be active, if it is 
  31.                             in the middle of its commit! Note that when a 
  32.                             transaction starts, we initialize trx->no to 
  33.                             IB_ULONGLONG_MAX. */  
  34.                             if (view->low_limit_no > trx->no) {  
  35.                                      view->low_limit_no = trx->no;  
  36.                             }  
  37.                    }  
  38.                    trx = UT_LIST_GET_NEXT(trx_list, trx);  
  39.          }  
  40.          view->n_trx_ids = n;  
  41.          if (n > 0) {  
  42.                    /* The last active transaction has the smallest id: */  
  43.                    view->up_limit_id = read_view_get_nth_trx_id(view, n - 1);  
  44.          } else {  
  45.                    view->up_limit_id = view->low_limit_id;  
  46.          }  
  47.          UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);  
  48.          return(view);  
  49.   
  50. }  

 

2、MVCC如何控制update操作

前面说先复制新数据,并插入DB_TRX_ID的值,在把旧数据的删除标志DB_TRX_ID

现在先介绍几个概念:

DB_ROLL_PTR是指向回滚段中旧版本7byte回滚指针。

redo log:重做日志,就是每次mysql在执行写入数据前先把要写的信息保存在重写日志中,但出现断电,奔溃,重启等等导致数据不能正常写入期望数据时,服务器可以通过redo_log中的信息重新写入数据。

undo log:撤销日志,与redo log恰恰相反,当一些更改在执行一半时,发生意外,而无法完成,则可以根据撤消日志恢复到更改之前的壮态。

mvcc中update步骤:

1、 记录事务中修改行数据的相应字段和值(包括旧版本事务id)在undo-log中记录。

2、 修改相应数据。

3、 在redo-log中保存要修改的相应(新版本事务id)数据写入

以上骤详细代码内容可看:

http://hi.baidu.com/gao1738/blog/item/dcec39d6185af2049d163d8c.html

4、 假如update不能正常运行怎根据undo-log redo-log 来回复

5、 当然如果当前版本事务没有commit的话则通过undo-log信息恢复原始数据状态

猜你喜欢

转载自blog.csdn.net/zhaoliang831214/article/details/80806157