--参考MySQL实战45讲:08事务到底是隔离的还是不隔离的?
<<Undo Redo MVCC.pptx>>
1、启动事务 2、事务ID 3、row trx_id 4、回滚段(undo log) 1、MVCC 增删改查原理 2、数据查询逻辑 3、数据更新逻辑 |
解决读-写冲突的无锁并发控制,读写互不阻塞,避免了脏读和不可重复读。
一、MVCC简介
MVCC (Multi version Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
数据的多版本(undo记录) 和 一致性视图(consistent read view) 实现了数据库的多版本并发控制(MVCC)。
1、启动事务
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。
创建read-view的位置,是begin后面的SQL语句的位置,begin的时候不会分配事务id,只有执行了sql之后才会分配事务id。
如果你想马上启动一个事务,产生readview,分配事务id,可使用命令:mysql> start transaction with consistent snapshot;
2、事务ID
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
InnoDB 的 MVCC,在每行记录后面保存两个隐藏列:创建时间、删除时间,实际存储的时间是系统版本号。每开始一个新的事务,系统版本号都会递增。事务开始时刻的系统版本号,会作为当前事务的版本号,用来和查询到的每行记录的版本号进行比较。保存两个额外的系统版本号,使大部分读操作都可以不用加锁。
3、row trx_id
每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。
DB_TRX_ID |
DB_ROLL_PTR |
DB_ROW_ID |
VALUE |
Innodb表在记录行上会多出三个字段:
- 6字节的事务ID
- 7字节的指向回滚段的指针
- 6字节的ROW_ID
4、回滚段(undo log)
回滚日志删除:当系统里没有比这个回滚日志更早的 read-view 的时候。
V1、V2、V3 为一个记录的多个修改版本。但不是物理真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。
U1、U2、U3 表示 undo log。
如果有一个事务,它的低水位(活跃事务列表最小值)是 18 ,row_trx_id=22 大于 18,那么当前记录对该事务不可见,就会从 V4 通过 U3 计算出 V3 的 row_trx_id=17 小于 低水位18 ,所以就读取 V3 的值。
二、MVCC 实现原理
InnoDB 在实现 MVCC 时用到的一致性读视图(consistent read view),用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
1、一致性视图
一致性视图(consistent read view):活跃事务列表 + 高水位
活跃事务列表:
InnoDB 在事务启动瞬间,为每个事务构造了一个数组(活跃事务列表):用来保存这个当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。
低水位:活跃事务列表数组里面事务 ID 的最小值记为低水位
高水位:当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
2、可见性判断(高低水位+活跃事务列表)
数据版本的可见性规则,就是基于数据的 row trx_id 和一致性视图的对比结果得到的。
数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
- 如果row上的trx_id < 低水位,说明该记录已提交,这条记录对于当前事务可见
- 如果row上的trx_id > 高水位, 说明该记录在当前事务之后提交,这条记录对于当前事务不可见,需要调用上一个可见的undo
- 如果row上的低水位< trx_id < 高水位
a.若row trx_id在数组(活跃事务列表)中,表示该记录还未提交,对当前事务不可见,需要调用上一个的undo,直到满足可见的条件。
b.若row trx_id不在数组中(非活跃事务列表,已提交),表示这个版本是已经提交了的事务生成的,对于当前事务可见。
三、MVCC 数据处理
1、MVCC 增删改查原理
select |
Innodb检查每行数据,符合以下两个标准,则返回该行数据。
|
insert |
InnoDB为每个新增行记录当前系统版本号作为创建版本号ID。 |
delete |
InnoDB为每个删除行记录当前系统版本号作为行的删除版本号ID。 |
update |
InnoDB将update转换成insert + delete。insert保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行的删除标识。 |
2、数据查询逻辑
以当前事务启动的时刻为准,如果一个数据版本是在当前事务启动之前生成的,就认;如果是当前事务启动以后才生成的,就不认,必须要找到它的上一个版本。如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。
对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
3、数据更新逻辑
mysql> create table t (id int(11) not null,k int(11) default null,primary key (id)) engine=innodb;
mysql> insert into t(id, k) values(1,1),(2,2);
假设:事务 A 开始前,系统里面只有一个活跃事务 ID 是 99;事务 A、B、C 的版本号分别是 100、101、102,且当前系统里只有这四个事务;三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。
read-view:事务 A 的视图数组就是 [99,100], 事务 B 的视图数组是 [99,100,101], 事务 C 的视图数组是 [99,100,101,102]。
Step |
事务A |
事务B |
事务C |
1 |
start transaction with consistent snapshot; |
|
|
2 |
|
start transaction with consistent snapshot; |
|
3 |
|
|
update t set k=k+1 where id=1; |
4 |
|
update t set k=k+1 where id=1; select k from t where id=1; |
事务C没有显式地使用 begin/commit |
5 |
select k from t where id=1; commit; |
|
表示这个 update 语句本身就是一个事务,语句完成的时候会自动提交。 |
6 |
|
commit; |
|
当前读:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
查询步骤:
事务 C 先更新生成102版本的数据(1,2)。事务 B 在更新的时候,当前读拿到的是102版本的数据(1,2),更新后生成了101版本的数据(1,3)。所以,在事务 B 查询的时候,发现最新数据的版本号也是 101,是自己的更新,可以直接使用,查询得到的 k 的值是 3。
查询结果:
事务 B 查到的 k 的值是 3,而事务 A 查到的 k 的值是 1
四、MVCC 事务隔离
1、InnoDB解决幻读问题
多版本并发控制(MVCC,Multiversion Concurrency control)
2、repeatable read 的实现
MySQL 默认的事务隔离级别 repeatable read,是通过MVCC实现的。
可重复读的核心就是一致性读(consistent read);
而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
3、RR(可重复读隔离级别)和 RC(读提交隔离级别)主要的区别
MVCC只在 repeatable reads 和 read committed 两个隔离级别下工作。
- RR(可重复读级别):只在事务开始时创建read-view(执行sql之后),之后事务里的查询都共用该一致性视图,直到事务结束。
- RC(读提交隔离级别):事务中每一个语句执行前都会重新算出一个新 read-view。
如果是可重复读隔离级别,事务启动的时候会创建一个视图 read-view,之后事务执行期间,即使有其他事务修改了数据,事务看到的仍然跟在启动时看到的一样。
- 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
- 对于读提交,查询只承认在语句启动前就已经提交完成的数据;
五、InnoDB MVCC 总结
Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。
比如,如果Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,则会破坏Transaction2的修改结果,导致Transaction2违反ACID。
理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。
MVCC有下面几个特点:
- 每行数据都存在一个版本,每次数据更新时都更新该版本
- 修改时Copy出当前版本随意修改,各个事务之间无干扰
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
Innodb 保存数据的方式:
- 事务以排他锁的形式修改原始数据
- 把修改前的数据存放于undo log,通过回滚指针与主数据关联
- 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读
乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,认为事务间争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,若没有就提交,如果有就放弃并重试。乐观并发控制类似自选锁。乐观并发控制适用于低数据争用,写冲突比较少的环境。