MVCC深入理解(转载)
1. MVCC简介
1.1 什么是MVCC
MVCC是一种多版本并发控制机制。
1.2 MVCC是为了解决什么问题?
- 大多数的MYSQL事务型存储引擎,如,InnoDB,Falcon以及PBXT都不使用一种简单的行锁机制.事实上,他们都和MVCC–多版本并发控制来一起使用.
- 大家都应该知道,锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销.
1.3 MVCC实现
MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制.
2.MVCC 具体实现分析
下面,我们通过InnoDB的MVCC实现来分析MVCC使怎样进行并发控制的.
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID.下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的.
2.1简单的小例子
create table yang(
id int primary key auto_increment,
name varchar(20));
假设系统的版本号从1开始.
INSERT
InnoDB为新插入的每一行保存当前系统版本号作为版本号.
第一个事务ID为1;
start transaction;
insert into yang values(NULL,'yang') ;
insert into yang values(NULL,'long');
insert into yang values(NULL,'fei');
commit;
- 1
- 2
- 3
- 4
- 5
对应在数据中的表如下(后面两列是隐藏列,我们通过查询语句并看不到)
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | undefined |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
SELECT
InnoDB会根据以下两个条件检查每行记录:
a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果.
DELETE
InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.
看下面的具体例子分析:
第二个事务,ID为2;
start transaction;
select * from yang; //(1)
select * from yang; //(2)
commit;
- 1
- 2
- 3
- 4
假设1
假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据;
第三个事务ID为3;
start transaction;
insert into yang values(NULL,'tian');
commit;
- 1
- 2
- 3
这时表中的数据如下:
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | undefined |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
4 | tian | 3 | undefined |
然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | undefined |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
假设2
假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后,接着又执行了事务4;
第四个事务:
start transaction;
delete from yang where id=1;
commit;
- 1
- 2
- 3
此时数据库中的表如下:
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
4 | tian | 3 | undefined |
接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from yang也会把id=1的数据检索出来.所以,事务2中的两条select 语句检索出来的数据都如下:
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
UPDATE
InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.
假设3
假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个用户对这张表执行了UPDATE操作:
第5个事务:
start transaction;
update yang set name='Long' where id=2;
commit;
- 1
- 2
- 3
根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | 5 |
3 | fei | 1 | undefined |
4 | tian | 3 | undefined |
2 | Long | 5 | undefined |
继续执行事务2的(2),根据select 语句的检索条件,得到下表:
id | name | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | 5 |
3 | fei | 1 | undefined |
还是和事务2中(1)select 得到相同的结果.
2:
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。
InnoDB在每行数据都增加两个隐藏字段,一个记录创建的版本号,一个记录删除的版本号。
- SELECT:
当隔离级别是REPEATABLE READ时select操作,InnoDB必须每行数据来保证它符合两个条件:
1、InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。
2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。
符合这两个条件的行可能会被当作查询结果而返回。 - INSERT:
InnoDB为这个新行记录当前的系统版本号。 - DELETE:
InnoDB将当前的系统版本号设置为这一行的删除ID。 - UPDATE:
InnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。
这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。他们只是简单地以最快的速度来读取数据,确保只选择符合条件的行。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,
做更多的检查工作,处理更多的善后操作。
MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下。READ UNCOMMITED不是MVCC兼容的,因为查询不能找到适合他们事务版本的行版本;它们每次都只能读到最新的版本。
SERIABLABLE也不与MVCC兼容,因为读操作会锁定他们返回的每一行数据。
3:
一、基础知识
事务: 事务是一组原子性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 **(3) **
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“删除”了改行。
[图片上传失败...(image-8af159-1581431533642)]
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
[图片上传失败...(image-eaf149-1581431533642)]
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隔离级别下
三、深入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事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。
[图片上传失败...(image-abfa45-1581431533642)]
在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);
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信息恢复原始数据状态.