MySQL-MVCC多版本控制及事务的隔离性

2023-10-11 09:40:03 浏览数 (3)

MySQL事务的启动方式

  1. 隐式:执行SQL语句自动提交(前提MySQL使用SET AUTOCOMMIT=1开启自动提交)
  2. 显式:begin/start transaction; update user set username = 'timi' where id =1; commit;

begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot

MySQL的InnoDB引擎具有不同的事务隔离级别,不同事务隔离级别通过视图创建时机的不同来实现。

MySQL的两种视图
  • View:它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view …,而它的查询方法与表一样。
  • 另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。
MySQL的MVCC快照

MVCC:Multiversion concurrency control,即多版本控制,在并发访问数据库时,通过对数据做多版本管理,也就是为每条记录保存多份历史数据供读事务访问,新的写入只需要添加新的版本即可,无需等待。避免因为写数据时要加写锁而阻塞读取数据的请求,实现读取数据不用加锁,读取数据同时修改。修改数据同时可读取。

多版本指的是数据表中同一个行数据可能会有多个版本(row),每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id,InnoDB复用了Undo log中已经记录的历史版本数据来满足MVCC的需求。

控制指的是,InnoDB使用Undo log控制不同的事务找到对应的数据版本。

InnoDB中每一个事务都有一个唯一的事务ID,叫做transaction id,每个事务在开始的时候向InnoDB事务系统申请的,其值按申请的顺序严格递增。

官方解释: A unique transaction ID number, internal to InnoDB. These IDs are not created for transactions that are read only and nonlocking

Undo log

为形象说明undo log,举个例子。

现有数据表如下:

代码语言:javascript复制
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

一次执行执行insertupdate及再次update语句,对应的事务编号分别为:Transaction I、Transaction J、Transaction K,那么新插入的id=1的数据行就有三个版本,undo log记录的数据数据分别为:

图片引用自taobao数据库月报。

undo log即为上图中的Rollptr,历史版本数据通过Rollptr穿成一个链表供MVCC使用。因此,undo log并不是在数据库中真实存在的,当需要查询某行数据历史版本时,可以通过Rollptr计算出。

Undo log与隔离级别的关系
数据可见性

一个事务在启动时声明:以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认,如果事务在我启动后生成的,就不认,必须找到它的上一个可见的版本。如果数据是这个事务自己更新的数据,它还是要认的。

InnoDB为每一个事务构造了一个数组,用来保存这个事务启动的瞬间,当前正在“活跃”指的是,启动了还没提交。

数据里事务ID的最小值记为低水位,当前系统里边已经创建过的事务ID的最大值加1记为高水位。视图数组和高水位,组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。

视图数组把所有的row trx_id分成了几种不同的情况。

对于事务启动瞬间来说,一个数据版本的row trx_id有以下几种可能:

对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有一下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的。
  2. 如果落在红色部分,标识这个版本是由将来启动的事务生成的,是肯定不可见的。
    • 如果row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见。
    • 如果row trx_id不在数组中,表示这个版本是已经提交的事务生成的,可见。

假设未提交事务数组中含有的row trx_id包含:90,91,92,96,93,94,95并未在未提交事务数组中,有一种可能性是:93,94,95事务执行较快,已提交。

一致性读

读操作基于某时间点得到一份那时的数据快照,无论其他数据对该行数据的修改,在查询过程中,若其他事务修改了数据,那么就要从undo log中获取旧版本的数据。

更新逻辑

规则:

更新数据都是先读后写的,读取的数据,读到的是当前最新版本值,称为“当前读”(current read)。

当某行数据被其他事务修改,拿到写锁还未释放时,“当前读”会等待写锁释放后再去执行。

可重复读与读提交

可重复读与读提交最大的区别是:

  • 可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后的事务里的其他查询都共用这个一致性视图;对于可重复读,查询只承认在事务启动前就已经提交完成的数据。
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。对于读提交,查询只承认在语句启动前就已经提交完成的数据。
举例
代码语言:javascript复制
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);

在可重复读隔离级别下,事务A查询到的k值为1,因为事务A首先启动,创建事务id,接着是事务B,事务B的row trx_id会大于事务A,落在高水位未开始事务中,数据修改对A不可见,事务C隐式开启事务,执行完成后隐式提交,由于同样C的row trx_id大于A,修改对于事务A版本依旧不可见,(即一致性读),所以事务A查询到的k值为历史版本1。

事务B查询到的k值为3,事务B首先开启事务,事务C随后开启,事务C将k=1修改为k=2,由于在修改时会使用“当前读”来查询数据的最新版本来保证数据的修改不会丢失,所以事务B在执行update语句前会查询到当前版本k=2,更新后k=3。

举例引用自极客时间《MySQL实现45讲》

0 人点赞