现有一个交易系统,每次交易都会更新余额。出帐扣减余额,入帐增长余额。为了保证资金安全,余额发生扣减时,须要比较现有余额与扣减金额大小,若扣减金额大于现有余额,扣减余额不足,扣减失败。html
余额表(省去其余字段)结构以下:mysql
CREATE TABLE `account` ( `id` bigint(20) NOT NULL, `balance` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
更新余额方法语序以下:sql
因为存在并发更新余额的状况,在 t3 时刻,使用写锁锁住该行记录。这样就能保证事务执行期间不会有其余事务提交变动。数据库
如今咱们假设有两个事务正在发执行该语序,执行顺序如图所示。数组
假设 id=1 记录 balance=1000,事务隔离等级为 RR。小伙伴们能够根据这个执行时序能够先思考下 t3,t5,t6,t7 结果。安全
注: 以上时序,顺序执行。可是事务 1 执行到 t3 时刻,t4 时刻,事务 2 执行时将会被阻塞,后续没法执行。
t4 时刻以后,只能先将 事务 1 语序执行,事务提交完成后,才能执行 事务 2 剩余语句。并发
下面放出问题的答案。mvc
t3 (1,1000)高并发
t5 (1,1000)3d
t4 (1,900)
t6 (1,1000)
有没有跟你结果的不太同样?
事务 1 查询结果基本没什么问题,事务 2 同一个事务内查询结果却不一样。
如今咱们先带着疑问,看完下面 MySQL 的相关原理,你就会明白一切。
假设在 RR 下,下图 id=1 balance=1000 。
上图时序顺序能够执行
事务 1 将 id=1 记录 balance 更新为 900。而后 t5 查询结果确定仍是 id=1 balance=1000,否则就读取到脏数据,不符合当前事务隔离级别。
从上面例子能够看到 id=1 的记录存在两个版本,一个为 balance=1000 ,一个为 balance=900。
MySQL 使用 MVCC 实现该功能。
MVCC:Multiversion concurrency control,多版本并发控制。摘录一段淘宝数据库月报的解释:
多版本控制: 指的是一种提升并发的技术。最先的数据库系统,只有读读之间能够并发,读写,写读,写写都要阻塞。引入多版本以后,只有写写之间相互阻塞,其余三种操做均可以并行,这样大幅度提升了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不一样,InnoDB是在undolog中实现的,经过undolog能够找回数据的历史版本。找回的数据历史版本能够提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也能够在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
能够看到 MVCC 主要用来提升并发,还能够用来读取老版本数据。下面介绍 MVCC 实现的原理。
首先咱们先看下 MySQL 记录结构。
能够看到 MySQL 行记录除了真实数据之外,还会存在三个隐藏字段,用来记录额外信息。
DB_TRX_ID:事务id。
DB_ROLL_PTR: 回滚指针,指向 undolog。
ROW_ID:行 id,与这次无关。
具体行记录结构,能够参考掘金的小册『 MySQL 是怎样运行的:从根儿上理解 MySQL』,说实话小册写的真的很好,收益颇丰。哈哈。
MySQL 经过 DB_ROLL_PTR 找到 undolog,而 undolog 记录数据的变动。这样 MySQL 就能推导出变动以前记录内容。
查找过程以下:
若须要知道 V1 版本记录,首先根据当前版本 V3 的 DB_ROLL_PTR 找到 undolog,而后根据 undolog 内容,计算出上一个版本 V2。以此类推,最终找到 V1 这个版本记录。
V1,V2 并非物理记录,没有真实存在,仅仅具备逻辑意义。
一行数据记录可能同时存在多个版本,但并非全部记录都能对当前事务可见。否则上面 t5 就可能查询到最新的数据。因此查找数据版本时候 MySQL 必须判断数据版本是否对当前事务可见。
MySQL 会在事务开始后创建一个一致性视图(并非马上创建),在这个视图中,会保存全部活跃的事务(还未提交的事务)。
假设当前事务建立活跃事务数组为以下图。
判断记录版本对于当前事务是否可见时,基于如下规则判断:
4 这个规则可能比较绕,结合上面图片比较好理解。
以上判断规则可能比较抽象,咱们将其总结下面几句话。
一致性视图只会在 RR 与 RC 下才会生成,对于 RR 来讲,一致性视图会在第一个查询语句的时候生成。而对于 RC 来讲,每一个查询语句都会从新生成视图。
MySQL 使用 MVCC 机制,能够 读取以前版本数据。这些旧版本记录不会且也没法再去修改,就像快照同样。因此咱们将这种查询称为快照读。
固然并非全部查询都是快照读,select .... for update/ in share mode 这类加锁查询只会查询当前记录最新版本数据。咱们将这种查询称为当前读。
讲完原理以后,咱们回过头分析一下上面查询结果的缘由。
这里咱们将上面答案再贴过来。
事务隔离级别为 RR,t1,t2 时刻两个事务因为查询语句,分别创建了一致性视图。
t3 时刻,因为事务 1 使用 select.. for update
为 id=1 这一行上了一把写锁,而后获取到最新结果。而 t4 时刻,因为该行已被上锁,事务 2 必须等待事务 1 释放锁才能继续。
t5 时刻根据一致性视图,不能读取到其余事务提交的版本,因此数据没变。t7 时刻余额扣减 100,t8 时刻提交事务。
此时最新版本记录为 id=1 balance=900。
因为事务 1 事务提交,行锁被释放,t4 获取到写锁。因为 t4 是当前读,因此查询的结果为最新版本数据(1,900)。
重点来了。t6 查询时,id=1 这条记录最新版本数据为 (1,900)。可是最新版本事务 id,属于事务 2建立以后未提交的事务,位于活跃事务数组中。因此最新记录版本对于事务2 是不可见的。没办法只能根据 undolog 去读取上一版本记录 (1,1000) 。这个版本记录恰好对于事务 2 可见。
若当前事务隔离级别修改为 RC,那么结果就与 RR 不一样。各位读者自行分析一下。
下面贴一下 RC 答案。
mysql mvcc
淘宝月报
innodb 相关实现
consistent-read
极客时间- MySQL 专栏--事务究竟是隔离的仍是不隔离的