• 事务级别设置不当引发的血案


    背景

    现有一个交易系统,每次交易都会更新余额。出账扣减余额,入账增加余额。为了保证资金安全,余额发生扣减时,需要比较现有余额与扣减金额大小,若扣减金额大于现有余额,扣减余额不足,扣减失败。

    余额表(省去其他字段)结构如下:

    
    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 剩余语句。

    下面放出问题的答案。

    t3 (1,1000)

    t5 (1,1000)

    t4 (1,900)

    t6 (1,1000)

    有没有跟你结果的不太一样?

    事务 1 查询结果基本没什么问题,事务 2 同一个事务内查询结果却不同。

    现在我们先带着疑问,看完下面 MySQL 的相关原理,你就会明白一切。

    相关原理

    MVCC

    假设在 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 会在事务开始后建立一个一致性视图(并不是立刻建立),在这个视图中,会保存所有活跃的事务(还未提交的事务)。

    假设当前事务创建活跃事务数组为如下图。

    视图数组

    判断记录版本对于当前事务是否可见时,基于以下规则判断:

    1. 若记录版本事务 id 小于当前活跃事务 id 数组最小值,如 id 为 40,小于 45,代表当前记录版本的事务已提交,当前记录对于当前事务可见。
    2. 若记录版本事务 id 大于当前活跃事务数组的最大值,如记录版本事务 id 为 100, 大于数组最大事务 id 90。说明了这个记录版本是当前事务创建之后生成,所以记录对于当前事务不可见。
    3. 若记录版本事务 id 是当前活跃数组事务之一,如记录数据版本事务 id 为 56。代表记录版本所属事务还未提交,所以记录对于当前事务不可见。
    4. 若记录版本事务 id 不是当前活跃数组事务之一,但是事务 id 位于数组最小值与最大值之一。如记录事务 ID 57。代表当前记录事务已提交,所以记录对于当前事务可见。
    5. 若记录版本事务 id 为当前事务 id,代表该行数据是当前事务变更的,当然得可见。

    4 这个规则可能比较绕,结合上面图片比较好理解。

    以上判断规则可能比较抽象,我们将其总结下面几句话。

    1. 未提交事务生成的记录版本,不可见。
    2. 视图生成前,提交事务生成记录版本可见。
    3. 视图生成后,提交事务生成记录版本不可见。
    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 答案。

    RC 下结果

    帮助文档

    mysql mvcc
    淘宝月报
    innodb 相关实现
    consistent-read
    极客时间- MySQL 专栏--事务到底是隔离的还是不隔离的

  • 相关阅读:
    接口XMPPConnection
    Smack 4.3.4 API
    Tigase XMPP Server
    Openfire Meetings插件是一个包含各种Jitsi项目(如VideoBridge和Meet)的实现
    华为方舟编译器 下载 和 LiteOS Studio Setup 2019-04-16.exe SDK下载
    华为 鸿蒙系统(HarmonyOS)
    C#.NET常见问题(FAQ)-如何使用2D绘图控件ZedGraph绘制坐标轴和坐标曲线
    C#.NET常见问题(FAQ)-如何声明list的多维数组
    C#.NET常见问题(FAQ)-如何设置控件水平对齐,垂直对齐
    C#.NET常见问题(FAQ)-如何修改Form不能修改窗体大小
  • 原文地址:https://www.cnblogs.com/goodAndyxublog/p/10660434.html
Copyright © 2020-2023  润新知