MySQL纵横研究院数据库技术专题社区 程序员技术栈

【原创】MySQL InnoDB存储引擎事务隔离性的实现

2019-05-25  本文已影响46人  正在加载更多

事务

事务保证一组数据库操作要么成功要么失败
当数据库中有多个事务同时进行时,可能会出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)问题,数据库的事务隔离级别能解决这些问题。

事务隔离级别

SQL 标准的事务隔离级别包括

InnoDB 存储引擎的事务隔离级别的实现

数据库会创建视图,访问的时候以视图的逻辑结果为准。
对于 读未提交 ,直接返回记录的最新值,不存在视图的概念;
对于 读提交 ,在每个 SQL 语句开始执行的时候会创建一个视图;
对于 可重复读 ,在每个事务启动时候会创建一个视图,整个事务过程中都使用该视图;
对于 串行读,是直接用锁来避免串行访问的;

那么,这个视图(即快照)是怎样创建与实现的呢?

对于 可重复读 隔离级别,事务在启动的时候会创建快照,这个快照是基于整个库的,这个快照不是拷贝数据库中的所有数据生成的。InnoDB 中每一个事务都有一个唯一的事务ID(transaction id),它是事务开始的时候向 InnoDB 的事务系统申请的,并且是严格自增的(TODO:事务id的值范围,超过了会发生什么),而且数据库的每行数据都有多个版本,每次事务更新数据的时候,都会生成一个新的版本,并且把事务的 transaction id 赋值给这个数据版本的事务 id,记为 row trx_id。同时,旧的数据版本会保留,并且在新的数据版本中,通过 undo log 能够得到旧版本的数据,下面是一个简单的图示:

图一.png

    这一行此时有四个 version,v4 是最新的,它被 transaction id 为 999 的事务更新,因此这个version的 row trx_id 是 999。
    当然,v1,v2,v3 并不是物理上真正存在的,而是需要的时候通过 v4 和 undo log 计算出来的。
    当一个事务启动的瞬间,InnoDB会为该事务构造一个数组,用来保存当前所有活跃的事务(即还没有提交的事务)的 transaction id ,数组中事务 id 最小的被记为低水位,当前系统已经创建过的事务id的最大值加1被记为高水位,这个视图数组和高水位就组成了当前事务的一致性视图,数据版本的可见性就是根据当前事务的id和这个一致性视图的对比结果得到的。
    所以在事务启动的瞬间,一致性视图把当前系统所有的row trx_id 分成了以下几种情况:

图二.png

    因此,对于图一来说,假设一个事务的低水位是 777,那么访问的那一行数据的时候,就会通过v4和undo log计算出v2版本时的值,所以在它看来,这一行的值是 13

    接下来,我们举一个栗子来实践下:

create TABLE trans_1 (id int(4) not null PRIMARY KEY,k int(4));
insert into trans_1 values(1,1);
事务A 事务B 事务C
start transaction with consistent snapshot
start transaction with consistent snapshot
update trans_1 set k = k + 1 where id = 1;
update trans_1 set k = k + 1 where id = 1;select * FROM trans_1 where id = 1;
select * FROM trans_1 where id = 1;commit;
commit

我们不妨假设:

这样,事务A是视图数组为[66,67],高水位的值是68,事务B的视图数组为[66,67,68],高水位的值是69,事务C的视图数组为[66,67,68,69],高水位的值是70。

事务C 的更新使得id=1这一行的最新版本是 69 了,50 已经成为历史版本,事务B 的更新使得 id=1 这一行的最新版本是 68 , 69 这个成为了历史版本。在事务A进行select的时候,select 的逻辑是:

a):id=1 这一行的最新版本 68,位于高水位,不可见。

b):通过undo log找到上一个版本,即 69 这个版本,比高水位大,不可见

c):再通过 undo log 找到上一个版本,即 50 这个版本,比低水位小,可见

所以 select 出来的就是 50 这个版本时候的值,即 k=1

说了这么多,数据可见性的整体感知就是:

在事务B 执行 update 之后,select出的 k 的值是3,会不会觉得奇怪呢?

事务B 在 update 之前,select 出 id=1 的 k 值是 1,即事务C 的 update 对事务B 是不可见的,事务B 的 update 应该是在 k=1 的基础上进行的。但为什么 select 出的值是 3 呢?这设计到一个当前读的概念,当更新数据的时候,都是先读后写,而这个读,只能读取当前的值,称为”当前读“。所以事务B update 之前 k 的值是 2 (单独去执行 select 的话 k = 1),update 的时候是以 k =2 为基础的,然后进行 select 的时候,发现数据的最新版本是 68,而自己的版本号也是 68,判断出是自己的更新,可以直接使用,所以 select 出的值就是 3

除了update语句外,如果select语句加上锁也是可以当前读的

如果 事务C update之后没有立即提交,那么情况会是怎样的呢?
                                                            表二

事务A 事务B 事务C ~
start transaction with consistent snapshot;
start transaction with consistent snapshot ;
start transaction with consistent snapshot;update trans_1 set k = k + 1 where id = 1;
update trans_1 set k = k + 1 where id = 1; select * FROM trans_1 where id = 1;
select * FROM trans_1 where id = 1; commit; commit;
commit;

由于事务C update之后没有提交,69 这个版本的写锁还没有释放,当事务B 去update的时候,由于要当前读,必须读取最新的版本,且要加锁,因此事务B就被阻塞了,直到事务C 提交之后,才能继续当前读

读提交 级别下,由于是每一个语句对应一个视图,
对于表一,事务B select的结果是 3,事务A select的结果是 2

ps:如果你的答案不是这个,你可能需要再看一遍文章

上一篇下一篇

猜你喜欢

热点阅读