文章目录

  • 视图
  • 快照
  • 快照的实现方式
  • 当前读
  • 事务的可重复读的能力是怎么实现的?
  • 读提交与可重复读的区别
  • 问题


如果是可重复读隔离级别,事务启动会创建视图read-view,保证数据在整个事务中一致。

但是,当一个事务更新一行,另一个事务恰好拥有这行的行锁,那它就会进行等待状态。

举个例子。

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

mysql8实战_更新数据


begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句(第一个快照读语句),事务才真正启动。

立刻启动事务的语句

start transaction with consistent snapshot

update语句本身就是一个事务,语句完成时会自动提交,这里默认自动提交

当执行完三个事务后,A显示的是1,B显示的是3。

视图

有两个

  1. view。用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。
  2. InnoDB在实现MVCC时用到的一致性读视图

快照

在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。

快照的实现方式

InnoDB里每个事务都有一个唯一的事务ID,也称为transaction id

事务ID是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的

每行数据也会有多个版本

每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id

同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它

简而言之,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id

如图

mysql8实战_更新数据_02


图中虚线框里是同一行数据的4个版本,当前最新版本是V4,k的值是22,它是被transaction id 为25的事务更新的,因此它的row trx_id也是25

语句更新会产生undo log(回滚日志),三个虚线箭头U1、U2、U3就是回滚日志

V1、V2、V3并不真实存在,而是每次都根据当前版本号和undo log计算出来

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

在实现上, InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前启动了但还没提交的所有事务ID。

mysql8实战_更新数据_03


通俗的讲

  1. 当数据是已提交事务生成或当前事务生成时,这个数据是可见的,那就在绿色区域
  2. 如果事务版本号比当前事务高,那就不可见,落在红色区域
  3. 当事务版本号比当前事务低,并不一定说明那个事务已提交,所以有个数组记录当前启动了但还没提交的所有事务ID,若这个事务版本号在事务ID数组中,那么不可见;如果不在数组中,那就是可见的

InnoDB利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力

当前读

更新数据的时候,就不能再在历史版本上更新了,否则其他事务的更新就丢失了

所以,更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

除了update语句外,select语句如果加锁,也是当前读。

如果把查询语句select * from t where id=1修改一下,加上lock in share mode 或 for update,也变成了当前读

假设事务C不是马上提交的,而是变成了下面的事务C’

mysql8实战_更新数据_04


事务C’的不同是,更新后并没有马上提交,在它提交前,事务B的更新语句先发起了

虽然事务C’还没提交,但是这个版本也已经生成了,并且是当前的最新版本

事务C’没提交,也就是说这个版本上的写锁还没释放。而事务B是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务C’释放这个锁,才能继续它的当前读。这就是“两阶段锁协议”

mysql8实战_更新数据_05

事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

读提交与可重复读的区别

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图

问题

用下面的表结构和初始化语句作为试验环境,事务隔离级别是可重复读。现在,我要把所有“字段c和id值相等的行”的c值清零,但是却发现了一个“诡异”的、改不掉的情况。请你构造出这种情况,并说明其原理。

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, c) values(1,1),(2,2),(3,3),(4,4);

mysql8实战_更新数据_06


复现出来以后,请你再思考一下,在实际的业务开发中有没有可能碰到这种情况?你的应用代码会不会掉进这个“坑”里,你又是怎么解决的呢?

答:RR下,用另外一个事物在update执行之前,先把所有c值修改,应该就可以。比如update t set c = id + 1。
这个实际场景还挺常见——所谓的“乐观锁”。时常我们会基于version字段对row进行cas式的更新,类似update …set … where id = xxx and version = xxx。如果version被其他事务抢先更新,则在自己事务中更新失败,trx_id没有变成自身事务的id,同一个事务中再次select还是旧值,就会出现“明明值没变可就是更新不了”的“异象”(anomaly)。解决方案就是每次cas更新不管成功失败,结束当前事务。如果失败则重新起一个事务进行查询更新。