MVCC(Multi-Version Concurrent Control),即多版本并发控制协议,广泛使用于数据库系统(mysql、HBase)。由于MVCC没有一个统一的实现标准,本人将针对mysql的InnoDB引擎谈谈它的应用。
1.是什么
MVCC是行锁的一种变种,但是他在很多情况下避免了加锁的操作,因此开销更低。多版本并发控制实现了非阻塞的读操作,也可称之为一致性非锁定读。它通过行的多版本控制方式来读取当前执行时间数据库中的行数据。实质上使用的是快照数据。
2.为什么需要
- 消除锁的开销;如果要保证数据的一致性,最简单的方式就是对操作数据进行加锁,但是加锁不可避免的会有锁开销。所以,如果有能避免进行加锁的方式当然是最好的。
- 提高并发度,后面会重点介绍。
3.它与事务隔离级别的关联
事务隔离级别“读已提交”与“可重复读”下 ,InnoDB存储引擎使用MVCC机制,
在“读已提交”事务隔离级别下,对于快照数据,MVCC读总是读取被锁定行的最新的快照数据。
而“可重复读”读到的总是读取事务开始时的行数据版本。
4.原理介绍
对于“可重复读”读到的总是读取事务开始时的行数据版本,那MVCC机制是如何保证可重复读的?
还要从它的数据结构上谈起,
1.行记录,Innodb引擎会为每一行添加3个字段实现的,DATA_TRX_ID、DATA_ROLL_PTR与DELETED_BIT
DATA _TRX_ID | 表示产生当前记录项的事务ID(每开启一个新的事务,其对应的事务id会自动递增) |
DATA_ROLL_PTR | 一个指向此条记录项的undo信息的指针,undo信息是指此条记录被修改前的信息; |
DELETED_BIT | 用于标识该记录是否被删除 |
2.read view
它很重要,innodb的read view确定一条记录能否看到,它遵循的原则:
- 看不到read view创建时刻以后启动的事务,场景1,Rule 1: When the read view object is created it notes down the smallest transaction identifier that is not yet used as a transaction identifier (trx_sys_t::max_trx_id). The read view calls it the low limit. So the transaction using the read view must not see any transaction with identifier greater than or equal to this low limit.
- 看不到read view创建时活跃的事务,场景2,Rule 2: The transaction using the read view must not see a transaction that was active when the read view was created.
RR(REPEATABLE-READ)读:每个事务在开始都会根据当前系统的活跃事务链表创建一个read_view,read_view是用来检索行的可见性的。
RC(READ-COMMITTED)读:事务中,每个语句都会创建read_view。
---------------------
Read View中的的变量(当前的活跃事务链表current-trx —> trx7 —> trx5 —> trx3 —> trx1)则按如下方式初始化:
read_view->creator_trx_id = current-trx; 当前的事务id
read_view->up_limit_id = trx1; 当前活跃事务的最小id
read_view->low_limit_id = trx7; 当前活跃事务的最大id
read_view->trx_ids = [trx1, trx3, trx5, trx7]; 当前活跃的事务的id列表
read_view->m_trx_ids = 4; 当前活跃的事务id列表长度
---------------------
low_limit_id,即当时活跃事务的最大id,如果读到row的data_trx_id>=low_limit_id,说明这些数据在当前事务开始时都还没有提交,如注释中的描述,这些数据都不可见。
up_limit_id,即当时活跃事务列表的最小事务id,如果row的data_trx_id<up_limit_id,说明这些数据在当前事务开始时都已经提交,如注释中的描述,这些数据均可见。
data_trx_id在up_limit_id和low_limit_id之间的row,如果这个data_trx_id在trx_ids的集合中,就说明开启当前的事务的时候,这个data_trx_id还处于活跃状态,即还未提交,那么这个row是不可见的;如果这个data_trx_id不在trx_ids的集合中,就说明开启当前的事务的时候,这个data_trx_id已经提交,那么这个row是可见的。
实例:
CREATE TABLE `test` (
`id` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
表中有数据(1,1)
autocommit为false, tx_isolation 是REPEATABLE-READ
场景1:
session A |
session B |
start transaction;(A) |
|
|
start transaction |
|
update test set b=2 where id=1; |
|
commit; |
select * from test;(B) |
|
B处结果为(1,2),似乎不符合那段话里的a条件,A事务看到了transaction version更大的B事务 |
场景2:
session A |
session B |
|
start transaction |
|
update test set b=2 where id=1; |
start transaction |
|
select * from test; |
|
|
commit; |
select * from test;(C) |
|
C处结果为(1,1),也就是说,A事务没有看到transaction version更小的B事务 |
5.缺点
为了实现多版本,innodb必须对每行增加相应的字段来存储版本信息,同时需要维护每一行的版本信息,而且在检索行的时候,需要进行版本的比较,因而降低了查询的效率;innodb还必须定期清理不再需要的行版本,及时回收空间,这也增加了一些开销
6.为什么select count(*)在myisam表上很快,而在Innodb的表上很慢?
因为innodb采用了MVCC技术,对于相同的行,可能同时存在多个版本,innodb必须根据查询的时间来过滤掉一些行,才能得出结果,必然要执行全表扫描,而全表扫描是非常耗时的.对于myisam的表,任何行都只有一个版本,mysql甚至不需要扫描就可以直接返回精确的统计结果,我们用explain也可以看到,对于myisam的表,执行select count(*)的时候,mysql显示” Select tables optimized away”,查询直接被优化了;而对于innodb的表,可能是全表扫描,也可能是”using index”,总之,速度肯定会比myisam的表慢很多.
7.能禁用MVCC吗?
禁用MVCC可以降低innodb引擎的开销,而同时innodb又可以支持外键约束,可以实现自动恢复.MVCC本身不支持read uncommitted等级,所以可以通过设置transaction_isolation = read uncommitted 来禁用MVCC.但是任何改变innodb默认隔离等级的操作,都会起到innodb_locks_unsafe_for_binlog=off类似的效果,这会导致诸如insert into t select * from t_src 之类的语句不再给源表t_src加锁,也不再使用innodb的间隙锁,从而产生幻读,直接导致binlog中记录的sql语句不能正确的串行化,从而主从数据库的数据不再一致,而且基于binlog的增量备份也不再有效.所以除非不需要记录binlog,否则别这么做.当然我们可以这样做来优化从库的性能,因为从库不需要记录binlog.