(Multiversion Concurrency Control)
1.什么是MVCC
多版本并发控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。 版本链
2. 为什么需要MVCC
MVCC主要是为了提高并发的读写性能,不用加锁就能让多个事务并发读写。比单纯的加锁更高效。
a. MySQL大多数实现都不是简单的行级锁。基于提升并发性能,一般采用MVCC,
b. 可以认为MVCC是行级锁的一个变种,典型的有乐观并发控制,悲观并发控制
c. MVCC 只在RC 和 RR级别下工作。因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
3.重要概念
3.1 read view(一致性视图),快照snapshot
事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:
查看当前所有的未提交并活跃的事务,存储在数组中
选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
选取所有已提交事务中最大的XID,加1后记录在xmax中
read view 主要是用来做可见性判断的
3.2 read view在RC, RR下生成时机不同,可见性不同:
3.2.1 在InnoDB(默认RR下),事务在begin/start transaction之后第一条select读操作后,生成一个快照(read view)
3.2.2 在InnoDB(RC下),事务在每一个select之后,生成一个快照(read view)
3.3 undo log
3.4 InnoDB存储引擎在数据库每行数据后面添加三个字段
事务ID(DB_TRX_ID) :6字节: 用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。
至于delete操作,在innodb看来也不过是一次update操作,每条记录的头信息( record header )里都有一个专门的 bit ( deleted_flag )来表示当前记录是否已经被删除。
回滚指针(DB_ROLL_PTR): 7字节,指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。
如果一行记录被更新, 则 undo log record 包含 '重建该行记录被更新之前内容' 所必须的信息。
隐藏主键列(DB_ROW_ID):6字节 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
当执行查询sql时,会生成一致性视图 read-view,它由执行查询时所有未提交事务id数组()和已创建最大事务id(max_id)组成。查询的数据结果需要跟read-view做对比从而得到快照结果
版本链比对规则:
1. 绿色(trx_id < min_id) , 表示这个版本是已提交的事务生成的,这个数据是可见的
2. 红色(trx_id > max_id) , 这个版本是由将来启动的事务生成的,肯定是不可见的。
3. 粉色(min_id<= trx_id <=max_id),两种:
a. 若row的trx_id 在数组中,表示这个版本由未提交的事务生成的,不可见,当前自己的事务是可见的。
b. 若row的trx_id 不在数组中,表示这个版本是已提交事务生成的,可见。
删除是 update的特殊情况,会将版本链最新的数据复制一份,将trx_id修改成删除操作事务的trx_id,在头信息(record header)里的(deleted_flag)标记bit上写上true,表示当前记录被删除,查询时按照以上规则如果deleted_flag为true,则不返回数据
当前读和快照读
1.MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读), 是通过 "行排他锁+MVCC" 一起实现的, 不仅可以保证可重复读, 还可以部分防止幻读, 而非完全防止;
2.为什么是部分防止幻读, 而不是完全防止?
效果: 在如果事务B在事务A执行中, insert了一条数据并提交, 事务A再次查询, 虽然读取的是undo中的旧版本数据(防止了部分幻读), 但是事务A中执行update或者delete都是可以成功的!!
因为在innodb中的操作可以分为当前读(current read)和快照读(snapshot read):
3.快照读(snapshot read)
简单的select操作(当然不包括select...lockinsharemode,select...forupdate)
4.当前读(current read) 官网文档 Locking Reads
select ... lock in share mode
select ... for update
insert
update
delete
在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,
当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
innodb在快照读的情况下并没有真正的避免幻读, 但是在当前读的情况下避免了不可重复读和幻读!!!
小结
一般我们认为MVCC有下面几个特点:
每行数据都存在一个版本,每次数据更新时都更新该版本
修改时Copy出当前版本, 然后随意修改,各个事务之间无干扰
保存时比较版本号,如果成功(commit),则覆盖原记录, 失败则放弃copy(rollback)
就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道, 因为这看起来正是,在提交的时候才能知道到底能否提交成功
而InnoDB实现MVCC的方式是:
事务以排他锁的形式修改原始数据
把修改前的数据存放于undo log,通过回滚指针与主数据关联
修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
二者最本质的区别是: 当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?
Innodb的实现真算不上MVCC, 因为并没有实现核心的多版本共存, undo log 中的内容只是串行化的结果, 记录了多个事务的过程, 不属于多版本共存。但理想的MVCC是难以实现的, 当事务仅修改一行记录使用理想的MVCC模式是没有问题的, 可以通过比较版本号进行回滚, 但当事务影响到多行数据时, 理想的MVCC就无能为力了。
比如, 如果事务A执行理想的MVCC, 修改Row1成功, 而修改Row2失败, 此时需要回滚Row1, 但因为Row1没有被锁定, 其数据可能又被事务B所修改, 如果此时回滚Row1的内容,则会破坏事务B的修改结果,导致事务B违反ACID。 这也正是所谓的 第一类更新丢失 的情况。
也正是因为InnoDB使用的MVCC中结合了排他锁, 不是纯的MVCC, 所以第一类更新丢失是不会出现了, 一般说更新丢失都是指第二类丢失更新。