Mysql到底是怎么实现MVCC的?这个问题无数人都在问,但google中并无答案,本文尝试从Mysql源码中寻找答案。
在Mysql中MVCC是在Innodb存储引擎中得到支持的,Innodb为每行记录都实现了三个隐藏字段:
- 6字节的事务ID(
DB_TRX_ID
) - 7字节的回滚指针(DB_ROLL_PTR)
- 隐藏的ID
1. Innodb的事务相关概念
- redo log
redo log就是保存执行的SQL语句到一个指定的Log文件,当Mysql执行recovery时重新执行redo log记录的SQL操作即可。当客户端执行每条SQL(更新语句)时,redo log会被首先写入log buffer;当客户端执行COMMIT命令时,log buffer中的内容会被视情况刷新到磁盘。redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件。 - undo log
与redo log相反,undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。 - rollback segment
回滚段这个概念来自Oracle的事物模型,在Innodb中,undo log被划分为多个段,具体某行的undo log就保存在某个段中,称为回滚段。可以认为undo log和回滚段是同一意思。 - 锁
Innodb提供了基于行的锁,如果行的数量非常大,则在高并发下锁的数量也可能会比较大,据Innodb文档说,Innodb对锁进行了空间有效优化,即使并发量高也不会导致内存耗尽。
对行的锁有分两种:排他锁、共享锁。共享锁针对对,排他锁针对写,完全等同读写锁的概念。如果某个事务在更新某行(排他锁),则其他事物无论是读还是写本行都必须等待;如果某个事物读某行(共享锁),则其他读的事物无需等待,而写事物则需等待。通过共享锁,保证了多读之间的无等待性,但是锁的应用又依赖Mysql的事务隔离级别。 - 隔离级别
隔离级别用来限制事务直接的交互程度,目前有几个工业标准:
- READ_UNCOMMITTED:脏读
- READ_COMMITTED:读提交
- REPEATABLE_READ:重复读
- SERIALIZABLE:串行化
Innodb对四种类型都支持,脏读和串行化应用场景不多,读提交、重复读用的比较广泛,后面会介绍其实现方式。
2. 行的更新过程
1. 初始数据行
2.事务1更改该行的各字段的值
- 用排他锁锁定该行
- 记录redo log
- 把该行修改前的值Copy到undo log,即上图中下面的行
- 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行
3.事务2修改该行的值
4. 事务提交
5. Insert Undo log
3. 事务级别
- READ_UNCOMMITTED
读未提交时,读事务直接读取主记录,无论更新事务是否完成 - READ_COMMITTED
读提交时,读事务每次都读取undo log中最近的版本,因此两次对同一字段的读可能读到不同的数据(幻读),但能保证每次都读到最新的数据。 - REPEATABLE_READ
每次都读取指定的版本,这样保证不会产生幻读,但可能读不到最新的数据 - SERIALIZABLE
锁表,读写相互阻塞,使用较少
4. MVCC
- 每行数据都存在一个版本,每次数据更新时都更新该版本
- 修改时Copy出当前版本随意修改,个事务之间无干扰
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
- 事务以排他锁的形式修改原始数据
- 把修改前的数据存放于undo log,通过回滚指针与主数据关联
- 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
5.总结
6. 参考资料
- Mysql官网
- http://blog.chinaunix.net/link.php?url=http://forge.mysql.com%2Fwiki%2FMySQL_Internals
- Understanding MySQL Internals
三个隐藏字段:
1、DB_TRX_ID:一个6byte的标识,每处理一个事务,其值自动+1,上述说到的“创建时间”和“删除时间”记录的就是这个DB_TRX_ID的值,如insert、update、delete操作时,删除操作用1个bit表示。
------------
Trx id counter 0 430621
Purge done for trx's n:o < 0 430136 undo n:o < 0 0
History list length 7
……
2、DB_ROLL_PTR:
3、DB_ROW_ID:
深入MVCC实现机制
1、到这里很多人就会发现,如果确实根据creat_num
InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),然后一致性读去比较记录的tx id的时候,并不是根据当前事务的tx id,而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。
storage/innobase/include/read0read.h 如下
及storage/innobase/read/read0read.c read_view_open_now()
storage/innobase/include/read0read.ic read_view_sees_trx_id()
在storage/innobase/read/read0read.c中实现了创建read view的函数read_view_open_now,在storage/innobase/include/read0read.ic中实现了判断一致性读是否可见的read_view_sees_trx_id
代码:
- read_view_t*
- read_view_open_now(
-
-
trx_id_t cr_trx_id, -
mem_heap_t* heap) - {
-
read_view_t* view; -
trx_t* trx; -
ulint n; -
ut_ad(mutex_own(&kernel_mutex)); -
view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap); -
view->creator_trx_id = cr_trx_id; -
view->type = VIEW_NORMAL; -
view->undo_no = 0; -
-
view->low_limit_no = trx_sys->max_trx_id; -
view->low_limit_id = view->low_limit_no; -
n = 0; -
trx = UT_LIST_GET_FIRST(trx_sys->trx_list); -
-
while (trx) { -
if (trx->id != cr_trx_id -
&& (trx->conc_state == TRX_ACTIVE -
|| trx->conc_state == TRX_PREPARED)) { -
read_view_set_nth_trx_id(view, n, trx->id); -
n++; -
-
if (view->low_limit_no > trx->no) { -
view->low_limit_no = trx->no; -
} -
} -
trx = UT_LIST_GET_NEXT(trx_list, trx); -
} -
view->n_trx_ids = n; -
if (n > 0) { -
-
view->up_limit_id = read_view_get_nth_trx_id(view, n - 1); -
} else { -
view->up_limit_id = view->low_limit_id; -
} -
UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view); -
return(view); -
- }
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、
2、
3、
4、
5、
- innodb可见性判断
到这里我们也就不难看出实际实现就是这两个数据结构进行比较:
InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view)在read_vied_sees_trx_id方法里我们有如下比较:
low_limit_id 是“高水位”即当时活跃事务的最大id,如果读到row的db_tx_id>=low_limit_id,说明这些id在此之前的数据都没有提交,如注释中的描述,这些数据都不可见。
if (trx_id >= view->low_limit_id) {
return(FALSE);
}
up_limit_id 是“低水位”即当时活跃事务列表的最小事务id,如果row的db_tx_id<up_limit_id,说明这些数据在事务创建的id时都已经提交,如注释中的描述,这些数据均可见。
if (trx_id < view->up_limit_id) {
return(TRUE);
}
在两个limit_id之间的我们需要从小到大逐个比较一下:
n_ids = view->n_trx_ids;
for (i = 0; i < n_ids; i++) {
trx_id_t view_trx_id
= read_view_get_nth_trx_id(view, n_ids – i – 1);if (trx_id <= view_trx_id) {
return(trx_id != view_trx_id);
}
}
这样我们在要在事务中获取100行数据,我们就能根据这100行的row db_tx_id即本事务的read_view来判断此版本的数据在事务中是否可见。
如果数据不可见我们需要去哪里找上版本的数据呢?就是通过刚才提到过的7BIT的DATA_ROLL_PTR去undo信息中寻找,同时再判断下这个版本的数据是否可见,以此类推。
- innodb更新
更新这个大家一般都比较熟悉,我这里简单表述一下,如一个测试表:
create table test (key int primary key,value varchar(10));
insert
InnoDB为每个新增行记录,如insert into test value(’1′,’aaa’), 会创建新的row,row db_tx_id即为当前系统版本号作为创建ID。
update
如update test set value=’bbb’ where key =’bbb’,InnoDB会复制了一行,这个新行的版本号使用了本次db_tx_id更新的版本号。它也把之前版本号作为了删除行的版本,即把原有row delete bit置为删除,不可见。
delete
InnoDB为每个删除行的记录当前系统版本号作为行的删除ID,也就是说把之前说的BIT位置成不可见的。
多版本并发控制(Multiversion Concurrency Controll MVCC)
第一点:
MVCC并不是MySql独有的,Oracle,PostgreSQL等都在使用。
MVCC并没有简单地使用行锁,而是使用“行级别锁”(row-level locking)。
MVCC的基本原理是:
MVCC的实现,通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。
MVCC的基本特征:
- 每行数据都存在一个版本,每次数据更新时都更新该版本。
- 修改时Copy出当前版本随意修改,各个事务之间无干扰。
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
InnoDB存储引擎MVCC的实现策略:
在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(可能为空)。这里的版本号并不是实际的时间值,而是系统版本号。每开始 个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。
每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。具体做法见下面的示意图。
MVCC具体的操作如下:
SELECT:InnoDB会根据以下两个条件检查每行记录:
1)InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,只么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
2)行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当系统的版本号为原来的行作为删除标识。
保存这两个额外系统版本号,使大多数操作都可以不用加锁。这样设计使得计数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC只在REPEATABLE READ和READ COMMITED两个隔离级别下工作,其它两个隔离级别和MVCC不兼容。
转自:
http://blog.csdn.net/chen77716/article/details/6742128
http://www.tuicool.com/articles/UvIN32
http://blog.sina.com.cn/s/blog_711b11fd0101bhks.html
http://blog.csdn.net/xifeijian/article/details/45230053