MySQL实战45讲阅读笔记-MVCC
系列
MySQL实战45讲阅读笔记-MySQL入门
MySQL实战45讲阅读笔记-日志
MySQL实战45讲阅读笔记-锁
MySQL实战45讲阅读笔记-索引
MySQL实战45讲阅读笔记-MVCC
MySQL的CrashSafe指在服务器宕机重启后,能够保证所有已经提交的事务的数据仍然存在,所有没有提交的事务的数据自动回滚;Innodb通过redolog和binlog实现crash safe功能,在执行一条update语句的流程大致如下;
为了保证server层日志(binlog)和引擎层日志(redolog)的一致性,MySQL使用了两阶段提交,即是把写日志拆分为prepare
和commit
两个阶段;
因为是XA(分布式事务)事务所以会为每一个事务分配一个唯一的Xid,就像上面binlog日志里面记录的一样,一个完整的事务后面会跟着一个Xid;
-
为什么两阶段提交能保证crash safe
因为保证了redolog里面存在的日志一定会在binlog里面存在;- 假如写binlog之前mysql发生crash,即在上图的
redolog prepare
阶段崩溃,由于此时的binlog还没有写,重启恢复时发现binlog缺少该事务,则回滚redolog的这条事务,因为没有写binlog所以也不会同步给备库; - 如果在写完binlog时mysql发生crash,但是还没到
redolog commit
阶段崩溃,由于redolog已经存在prepare且对应事务的binlog是完整的,所以恢复后会自动提交该事务;
- 假如写binlog之前mysql发生crash,即在上图的
崩溃恢复的规则
- redolog的事务是完整的,也就是有了commit标识,则直接提交该事务;
- redolog只有完整的prepare,则判断对于binlog事务是否完整,如果完整则提交事务,如果不完整则回滚事务;
-
如何判断binlog是否完整
statement格式的binlog最后面存在COMMIT
标识,row格式的binlog最后面有Xid event
; -
redolog和binlog如何关联
有一个共同的Xid,因为是唯一的所以可以根据这个字段找到对应的事务; -
处于prepare阶段的redolog加上完整的binlog在重启后为什么就能恢复
当写完binlog后发生crash,这时备库已经收到了主库发来的binlog日志并执行该事务,所以为了保持主备一致,主库在恢复的过程需要重新提交这条事务;
MVCC
多版本并发控制(Multiversion Currency Control)是一种提高并发的技术,远比使用行锁效率要高的多, MVCC的原理大概是同一行记录可能会有多个版本的视图(Read-View),从而摆脱锁实现并发读(基于快照),实现事务之间的读写分离;
事务隔离级别问题
- 脏读
事务A读取到了事务B未提交的数据;
事务A | 事务B |
---|---|
开启事务A | |
开启事务B | |
查询某行得到数据0 | |
更新该行值为10 | |
查询某行得到数据10 | |
回滚 |
- 不可重复读
在事务A执行的过程中因为其他事务的update导致事务A查询同一个数据出现不一致的结果;
事务A | 事务B |
---|---|
开启事务A | |
开启事务B | |
查询某行得到数据0 | |
更新该行值为10 | |
查询某行得到数据0 | |
提交 | |
查询某行得到数据10 |
- 幻读
指事务A在执行的过程中因为读取到了其他事务insert导致事务原本的结果集出现偏差的情况;比如下面例子在RR隔离级别下发生的幻读情况,事务A在事务执行过程中查找id=2得到两种结果,幻读‘读’的东西专指‘新增行’;
事务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) |
事务的隔离级别
-
读未提交(Read uncommitted)
即一个事务做出的改变还未提交就能被其他事务所看到 -
读提交(Read committed)
即一个事务做出的改变需要提交后才能被其他事务看到 -
可重复读(Repeatable read)
即一个事务执行的过程中看到的数据总是和这个事务启动时看到数据是一致的; -
串行化(Serializable)
对同一行数据,写会加写锁,读会加读锁,当读写锁出现冲突时后访问的事务会等待占用锁事务执行完成才会继续完成;
MVCC只会在读提交
和可重复读
这两个隔离级别下才会存在,读未提交是直接返回该行最新的数据,而串行化是直接采用加锁的方式避免并行访问;
事务隔离的实现
在可重复读
隔离级别下事务启动时会创建一个视图,在读提交
下视图会在每个SQL执行的时候才创建,视图可以把它当作当前数据的一个快照或副本;在MVCC中,读操作可以分为两种:当前读和快照读,普通的select语句,只要不涉及加锁操作就属于快照读,快照读读的是当前事务的可见版本(基于某个版本的副本),而当前读会给读取的行加上锁,比如select...for update
,读取的一定是该行最新的版本;
Innodb中每个事务有一个唯一的事务IDtransaction id
,每行数据也是存在多个版本,每次更新数据的时候,都会生成一个新的数据版本,并把transaction id
赋值给这个版本的row trx_id
,且旧版本数据要保留;也就是说数据表里面每一行记录都有可能存在多个版本(row),每个版本都有自己的trx_id;
如图这行被多个事务修改了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记为高水位;这是数组和高水位就组成了该事务的一致性视图;
事务之间数据版本的可见性就是基于数据行的trx_id和这个一致性视图对比的结果;
一个数据版本的trx_id存在以下几个可能
- 如果trx_id在绿色的部分则表示这个版本是属于已提交的事务或者自己生成的,这个数据是可见的;
- 如果在红色的区域则是不可见的;
- 如果在黄色的区域,则包括两种情况
- 如果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 幻读的详解、实例及解决办法