mysql 知识库Java 核心技术SQL极简教程 · MySQL · MyBatis · JPA 技术笔记 教程 总结

MySQL实战45讲阅读笔记-MVCC

2019-08-29  本文已影响0人  Mhhhhhhy

系列
MySQL实战45讲阅读笔记-MySQL入门
MySQL实战45讲阅读笔记-日志
MySQL实战45讲阅读笔记-锁
MySQL实战45讲阅读笔记-索引
MySQL实战45讲阅读笔记-MVCC

MySQL的CrashSafe指在服务器宕机重启后,能够保证所有已经提交的事务的数据仍然存在,所有没有提交的事务的数据自动回滚;Innodb通过redolog和binlog实现crash safe功能,在执行一条update语句的流程大致如下;


为了保证server层日志(binlog)和引擎层日志(redolog)的一致性,MySQL使用了两阶段提交,即是把写日志拆分为preparecommit两个阶段;
因为是XA(分布式事务)事务所以会为每一个事务分配一个唯一的Xid,就像上面binlog日志里面记录的一样,一个完整的事务后面会跟着一个Xid;

崩溃恢复的规则
  1. redolog的事务是完整的,也就是有了commit标识,则直接提交该事务;
  2. redolog只有完整的prepare,则判断对于binlog事务是否完整,如果完整则提交事务,如果不完整则回滚事务;

MVCC

多版本并发控制(Multiversion Currency Control)是一种提高并发的技术,远比使用行锁效率要高的多, MVCC的原理大概是同一行记录可能会有多个版本的视图(Read-View),从而摆脱锁实现并发读(基于快照),实现事务之间的读写分离;

事务隔离级别问题
事务A 事务B
开启事务A
开启事务B
查询某行得到数据0
更新该行值为10
查询某行得到数据10
回滚
事务A 事务B
开启事务A
开启事务B
查询某行得到数据0
更新该行值为10
查询某行得到数据0
提交
查询某行得到数据10
事务A 事务B
开启事务A
开启事务B
查询id=2得到0行
插入id=2
commit
插入id=2(Duplicate entry '2' for key 'PRIMARY')
select * from t where id = 2 for update; (1 rows)
事务的隔离级别

MVCC只会在读提交可重复读这两个隔离级别下才会存在,读未提交是直接返回该行最新的数据,而串行化是直接采用加锁的方式避免并行访问;

事务隔离的实现

可重复读隔离级别下事务启动时会创建一个视图,在读提交下视图会在每个SQL执行的时候才创建,视图可以把它当作当前数据的一个快照或副本;在MVCC中,读操作可以分为两种:当前读快照读,普通的select语句,只要不涉及加锁操作就属于快照读,快照读读的是当前事务的可见版本(基于某个版本的副本),而当前读会给读取的行加上锁,比如select...for update,读取的一定是该行最新的版本;

Innodb中每个事务有一个唯一的事务IDtransaction id,每行数据也是存在多个版本,每次更新数据的时候,都会生成一个新的数据版本,并把transaction id赋值给这个版本的row trx_id,且旧版本数据要保留;也就是说数据表里面每一行记录都有可能存在多个版本(row),每个版本都有自己的trx_id;

图1
如图这行被多个事务修改了3次,所以会留下4个版本的数据,U1、U2和U3组成了Undolog(回滚日志),v1、v2、v3版本并不是真实存在的,而是在需要的时候通过undo log计算出来的;

InnoDB每个行上有额外的几个隐含字段,rowid表示对应的行,db_trx_id表示事务的id,db_roll_pt则是回滚指针,指向undolog里面被修改前的行,delete bit表示是否删除;

Innodb为每个事务都构造了一个数组用来保存在这个事务启动的瞬间,正在活跃事务id,活跃事务是指启动了但是还未提交的事务;
数组里面事务id的最小值记为低水位,当前系统里面已经创建过的事务id的最大值+1记为高水位;这是数组和高水位就组成了该事务的一致性视图

图2

事务之间数据版本的可见性就是基于数据行的trx_id和这个一致性视图对比的结果;
一个数据版本的trx_id存在以下几个可能

有了这些规则之后,每一个事物都可以知道哪一个数据版本对于其他事物来说是可见的还是不可见的;

执行更新或删除语句时,会把该行修改前的状态记录到Undo log里面,使回滚指针指向它,同时把更新语句和undo log的更新都记录到redolog中(崩溃恢复时先回复redolog再通过redolog构造undolog回滚未提交的事务);查找之前的版本只需要通过回滚指针找到之前的版本即可,删除操作也是可以通过Undolog回滚的,因为删除操作只是在commit阶段才执行删除操作;

来看一个实例,隔离级别是RR

mysql> select * from t; 比如在表t有以下两行数据
+----+------+
| id | k    |
+----+------+
|  1 |    1 |
|  2 |    2 |
+----+------+
事务A(trx_id=100) 事务B(trx_id=101) 事务C(trx_id=102)
start transaction with consistent snapshot
start transaction with consistent snapshot
update t set k=k+1 where id=1
update t set k=k+1 where id=1
select k from t where id=1
select k from t where id=1; commit
commit

start transaction并不是马上开启事务而是在对innodb表进行操作的第一条语句才启动的,而一致性视图是在执行第一次select语句才建立;
start transaction with consistent snapshot在可重复读隔离级别下则是立马启动一个事务并创建视图;

假如事务A开启的时候系统里面只存在一个活跃的事务id99,且在这三个事务开启前(1,1)这一行数据的row trx_id是90;

所以各个事务的视图数据是
A:[99, 100]
B:[99, 100, 101]
C:[99, 100, 101, 102]

事务C是第一个有效事务,更新k到2时,这一行的最新版本的trx_id是102;
事务B再次更新k值,这时事务C已经提交了,且在更新数据的时候都是先读后写,这个读只能是当前读,不然事务C更新的数据就丢失了,所以更新后k=3,这时最新版本的trx_id是101;
事务A查询k的时候事务B还未提交,所以k=3(trx_id:101)这个版本对于事务A来说是不可见的,于是根据undolog把这一行的数据取之前的版本k=2(trx_id:102),但是事务A的视图是[99,100],所以这个版本的数据对于A来说也是不可见的,再一次回退之前的版本k=1(trx_id:90),这个版本对于A来说是小于事务A的trx_id,即可见的,所以事务A读到的k是1;

在事务A期间虽然k被更新过,但是对于A来说看到这行的数据是这个事务启动时的值,这个被称为一致性读;但是在更新的时候,读取的值是最新的,不然的话会造成数据丢失,这个读就是当前读

总结出来就是对于一个事务来说除了自己更新的总是可见之外还有三种情况

事务的可重复读就是依赖于一致性读实现的,在更新数据时使用当前读
可重复读是在事务开始的时候创建一致性视图的,之后的更新都是使用这个视图,而读提交是每一个语句执行前都会计算出一个视图;

假如上面的例子发生在RC隔离级别下,分析上面那个例子

事务A(trx_id=100) 事务B(trx_id=101) 事务C(trx_id=102)
start transaction with consistent snapshot
start transaction with consistent snapshot
update t set k=k+1 where id=1
update t set k=k+1 where id=1
select k from t where id=1
select k from t where id=1; commit
commit

start transaction with consistent snapshot在RR下是创建一个持续整个事务的一致性快照,但是在RX下面没有效果,所以只是个普通的开启事务,即begin transaction

事务C执行set k后假设k=1,事务B执行set k后k=2未提交,所以等事务A执行select的时候创建一个视图,此时视图数组是[100, 101],事务C是属于已经提交的事务,所以事务A可以查询到事务C已经提交的版本,但是事务B对于事务A来说同属活跃事务,且在视图数组中,按照规则如果trx_id在数组中,则表示这个版本是由还没有提交的事务生成的,不可见,所以事务A查询到的k=1;

参考

[图解MySQL]MySQL组提交(group commit)
数据库内核月报 - 2017 / 12
mysql 幻读的详解、实例及解决办法

上一篇下一篇

猜你喜欢

热点阅读