事务
事务保证一组数据库操作要么成功要么失败
当数据库中有多个事务同时进行时,可能会出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)问题,数据库的事务隔离级别能解决这些问题。
事务隔离级别
SQL 标准的事务隔离级别包括
- 读未提交(read uncommitted):一个事务还没有提交,他所做的变更能被其他事务看到
- 读提交(read committed):一个事务提交了之后,他所做的变更才能被其他事务看到
- 可重复读(repeatable read):一个事务在执行过程中,他所看到的数据总是和在该事务启动时看到的数据视图是一致的。同时,该事务未提交的变更对其他事务也是不可见的
- 串行读(serializable read):对于同一行数据,读会加上读锁,写会加上写锁,当出现读写锁冲突的时候,后一个事务必须等待前一个事务执行完成才能继续执行
InnoDB 存储引擎的事务隔离级别的实现
数据库会创建视图,访问的时候以视图的逻辑结果为准。
对于 读未提交 ,直接返回记录的最新值,不存在视图的概念;
对于 读提交 ,在每个 SQL 语句开始执行的时候会创建一个视图;
对于 可重复读 ,在每个事务启动时候会创建一个视图,整个事务过程中都使用该视图;
对于 串行读,是直接用锁来避免串行访问的;
那么,这个视图(即快照)是怎样创建与实现的呢?
对于 可重复读 隔离级别,事务在启动的时候会创建快照,这个快照是基于整个库的,这个快照不是拷贝数据库中的所有数据生成的。InnoDB 中每一个事务都有一个唯一的事务ID(transaction id),它是事务开始的时候向 InnoDB 的事务系统申请的,并且是严格自增的(TODO:事务id的值范围,超过了会发生什么),而且数据库的每行数据都有多个版本,每次事务更新数据的时候,都会生成一个新的版本,并且把事务的 transaction id 赋值给这个数据版本的事务 id,记为 row trx_id。同时,旧的数据版本会保留,并且在新的数据版本中,通过 undo log 能够得到旧版本的数据,下面是一个简单的图示:
这一行此时有四个 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 分成了以下几种情况:
- 若落在黄色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的
- 若落在紫色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的
- 若落在绿色部分,那包含两种情况
a) 若row trx_id 在数组中,表示这个版本是由未提交的事务生成的,不可见
b)若row trx_id 不在数组中,表示这个版本是由已经提交的事务生成的,可见
因此,对于图一来说,假设一个事务的低水位是 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开始之前,系统中只有一个活跃的事务,id为 66;
- 事务A,事务B,事务C 的事务ID分别是 67,68,69;
- 事务A开始之前,(1,1)这一行数据的数据的row trx_id 为 50;
这样,事务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:如果你的答案不是这个,你可能需要再看一遍文章
网友评论