事务与隔离

2019-03-28  本文已影响0人  itczl

事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一

隔离性与隔离级别

ACID

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念

SQL 标准的事务隔离级别

隔离的实现

事务启动的时候会创建一个视图,访问的时候以视图的逻辑结果为准

隔离示例

假设数据表 T 中只有一列,其中一行的值为 1,下面是按照时间顺序执行两个事务的行为,在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是多少:

隔离的应用

假设你在管理一个个人银行账户表
一个表存了每个月月底的余额,一个表存了账单明细
候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致
你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果, 这时候使用“可重复读”隔离级别就很方便
事务启动时的视图可以认为是静态的,不受其他事务更新的影响

事务的启动

事务隔离的实现

在 MySQL 中,每条记录在更新的时候都会同时记录一条回滚操作(undo log)。记录上的最新值,通过回滚操作,都可以得到前一个状态的值,这样同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)

多版本是咋么实现的

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。 而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。 也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id

下图是一个记录被多个事务连续更新后的状态:

事务隔离是咋么实现的

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

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

比如,对于上图中的数据来说,如果有一个事务,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。有了这个声明后,系统里面随后发生的更新,就跟这个事务看到的内容无关了,因为之后的更新,生成的版本一定属于上面的 2 或者 3(1) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了

小插曲

MySQL中的两个视图

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

回滚日志总不能一直保留吧,什么时候删除呢?

答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除就是当系统里没有比这个回滚日志更早的 read-view 的时候

为什么建议你尽量不要使用长事务

长事务意味着系统里面会存在很老的事务视图
由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间
在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库

事务隔离下的查询&更新

建表语句:

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);

3个事务的执行时机:

假设

这样,事务 A 的视图数组就是 [99,100], 事务 B 的视图数组是 [99,100,101], 事务 C 的视图数组是 [99,100,101,102]

查询

事务 A 的语句返回的结果,为什么是 k=1?
为了简化分析,只画出跟事务 A 查询逻辑有关的操作:

从图中可以看到

现在事务 A 要来读数据了,它的视图数组是 [99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的:

这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读

更新

事务 B 的 update 语句,如果按照一致性读,好像结果不对。 你看图 5 中,事务 B 的视图数组是先生成的,之后事务 C 才提交,不是应该看不见 (1,2) 吗,怎么能算出 (1,3) 来?

这里我们提到了一个概念,叫作当前读。其实,除了 update 语句外,select 语句如果加锁,也是当前读

下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;
假设事务 C 不是马上提交的,而是变成了下面的事务 C’,会怎么样呢

事务 C的不同是,更新后并没有马上提交,在它提交前,事务 B 的更新语句先发起了。虽然事务 C’还没提交,但是 (1,2) 这个版本也已经生成了,并且是当前的最新版本。那么,事务 B 的更新语句会怎么处理呢?

到这里,一致性读、当前读和行锁就串起来了

读提交下的事务

上边说的例子是可重复读情况下的,读提交隔离级别下呢

先总结下可重复读

读提交的逻辑和可重复读的逻辑类似,最主要的区别是:

在读提交隔离级别下,事务 A 和事务 B 的查询语句查到的 k,分别应该是多少呢

下面是读提交时的状态图,可以看到这两个查询语句的创建视图数组的时机发生了变化,就是图中的 read view 框。(注意:这里,我们用的还是事务 C 的逻辑直接提交,而不是事务 C’)

内容有点多,总结一下

"start transaction with consistent snapshot; "

意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的 start transaction

上一篇下一篇

猜你喜欢

热点阅读