• 技术文章 >数据库 >mysql教程

    一文详解MySQL中的事务和 MVCC 原理

    青灯夜游青灯夜游2022-03-09 11:05:01转载543
    本篇文章带大家了解一下MySQL中的事务,并介绍一下MVCC 原理,希望能够给大家提供帮助!

    01 什么是事务?

    数据库事务指的是一组数据操作,事务内的操作要么就是全部成功,要么就是全部失败,什么都不做,其实不是没做,是可能做了一部分但是只要有一步失败,就要回滚所有操作,有点一不做二不休的意思。

    在 MySQL 中,事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一

    1.1 四大特性

    1.2 隔离级别

    SQL 事务的四大特性中原子性、一致性、持久性都比较好理解。但事务的隔离级别确实比较难的,今天主要聊聊 MySQL 事务的隔离性。

    SQL 标准的事务隔离从低到高级别依次是:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。级别越高,效率越低

    1.3 解决的并发问题

    SQL 事务隔离级别的设计就是为了能最大限度的解决并发问题:

    SQL 不同的事务隔离级别能解决的并发问题也不一样,如下表所示:只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷

    事务隔离级别脏读不可重复读幻读
    读未提交可能可能可能
    读已提交不可能可能可能
    可重复读不可能不可能可能
    串行化不可能不可能不可能

    PS:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

    1.4 举个栗子

    这么说可能有点难以理解,举个栗子。还是之前的表结构以及表数据

    CREATE TABLE `student`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `age` int(11) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

    表数据

    假设现在,我要同时启动两个食物,一个事务 A 查询 id = 2 的学生的 age,一个事务 B 更新 id = 2 的学生的 age。流程如下,在四种隔离级别下的 X1、X2、X3 的值分别是怎样的呢?

    隔离级别举例

    那为什么会出现这样的结果呢?事务隔离级别到底是怎么实现的呢?

    事务隔离级别是怎么是实现的呢?我在极客时间丁奇老师的课上找到了答案:

    实际上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在 “可重复读” 隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在 “读提交” 隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交” 隔离级别下直接返回记录上的最新值,没有视图概念;而 “串行化” 隔离级别下直接用加锁的方式来避免并行访问

    1.5 设置事务隔离级别

    不同的数据库默认设置的事务隔离级别也大不一样,Oracle 数据库的默认隔离级别是读提交,而 MySQL 是可重复读。所以,当你的系统需要把数据库从 Oracle 迁移到 MySQL 时,请把级别设置成与搬迁之前的(读提交)一致,避免出现不可预测的问题

    1.5.1 查看事务隔离级别

    # 查看事务隔离级别
    5.7.20 之前
    SELECT @@transaction_isolation
    show variables like 'transaction_isolation';
    
    # 5.7.20 以及之后
    SELECT @@tx_isolation
    show variables like 'tx_isolation'
    
    +---------------+-----------------+
    | Variable_name | Value           |
    +---------------+-----------------+
    | tx_isolation  | REPEATABLE-READ |
    +---------------+-----------------+

    1.5.2 设置隔离级别

    修改隔离级别语句格式是:set [作用域] transaction isolation level [事务隔离级别]

    其中作用域可选:SESSION(会话)、GLOBAL(全局);隔离级别就是上面提到的 4 种,不区分大小写。

    例如:设置全局隔离级别为读提交

    set global transaction isolation level read committed;

    1.6 事务的启动

    MySQL 的事务启动有以下几种方式:

    # 更新学生名字
    START TRANSACTION;
    update student set name = '张三' where id = 2;
    commit;

    02 事务隔离的实现

    理解了隔离级别,那事务的隔离是怎么实现的呢?要想理解事务隔离,先得了解 MVCC 多版本的并发控制这个概念。而 MVCC 又依赖于 undo log 和 read view 实现。

    2.1 什么是 MVCC?

    百度上的解释是这样的:

    MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

    MVCC 使得数据库读不会对数据加锁,普通的 SELECT 请求不会加锁,提高了数据库的并发处理能力;数据库写才会加锁。 借助 MVCC,数据库可以实现 READ COMMITTED,REPEATABLE READ 等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了 ACID 中的 I 特性(隔离性)。

    MVCC 只在 REPEATABLE READ 和 READ COMMITIED 两个隔离级别下工作。其他两个隔离级别都和 MVCC 不兼容 ,因为 READ UNCOMMITIED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

    2.1.1 InnDB 中的 MVCC

    InnDB 中每个事务都有一个唯一的事务 ID,记为 transaction_id。它在事务开始时向 InnDB 申请,按照时间先后严格递增。

    而每行数据其实都有多个版本,这就依赖 undo log 来实现了。每次事务更新数据就会生成一个新的数据版本,并把 transaction_id 记为 row trx_id。同时旧的数据版本会保留在 undo log 中,而且新的版本会记录旧版本的回滚指针,通过它直接拿到上一个版本。

    所以,InnDB 中的 MVCC 其实是通过在每行记录后面保存两个隐藏的列来实现的。一列是事务 ID:trx_id;另一列是回滚指针:roll_pt。

    2.2 undo log

    回滚日志保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。

    根据操作的不同,undo log 分为两种: insert undo log 和 update undo log。

    2.2.1 insert undo log

    insert 操作产生的 undo log,因为 insert 操作记录没有历史版本只对当前事务本身可见,对于其他事务此记录不可见,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作。

    purge 的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收 undo pages

    所以,插入数据时。它的初始状态是这样的:

    insert undo log

    2.2.2 update undo log

    UPDATE 和 DELETE 操作产生的 Undo log 都属于同一类型:update_undo。(update 可以视为 insert 新数据到原位置,delete 旧数据,undo log 暂时保留旧数据)。

    事务提交时放到 history list 上,没有事务要用到这些回滚日志,即系统中没有比这个回滚日志更早的版本时,purge 线程将进行最后的删除操作。

    一个事务修改当前数据:

    第二次事务

    另一个事务修改数据:

    第三次事务

    这样的同一条记录在数据库中存在多个版本,就是上面提到的多版本并发控制 MVCC。

    另外,借助 undo log 通过回滚可以回到上一个版本状态。比如要回到 V1 只需要顺序执行两次回滚即可。

    2.3 read-view

    read view 是 InnDB 在实现 MVCC 时用到的一致性读视图,用于支持 RC(读提交)以及 RR(可重复读)隔离级别的实现

    read view 不是真实存在的,只是一个概念,undo log 才是它的体现。它主要是通过版本和 undolog 计算出来的。作用是决定事务能看到哪些数据

    每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性

    2.3.1 数据版本的可见性规则

    read view 中主要包含当前系统中还有哪些活跃的读写事务,在实现上 InnDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正活跃(还未提交)的事务

    前面说了事务 ID 随时间严格递增的,把系统中已提交的事务 ID 的最大值记为数组的低水位,已创建过的事务 ID + 1记为高水位

    这个视图数组和高水位就组成了当前事务的一致性视图(read view)

    这个数组画个图,长这样:

    数据版本的可见性规则

    规则如下:

    第三点我在看教程的时候也有点疑惑,好在有热心网友解答:

    落在绿色区域意味着是事务 ID 在低水位和高水位这个范围里面,而真正是否可见,看绿色区域是否有这个值。如果绿色区域没有这个事务 ID,则可见,如果有,则不可见。在这个范围里面并不意味着这个范围就有这个值,比如 [1,2,3,5],4 在这个数组 1-5 的范围里,却没在这个数组里面。

    这样说可能有点难以理解,我假设一个场景:三个事务对同一条数据进行查询更新等操作,为此画了张图以方便理解:

    三个事务操作

    原始数据还是下图这样的,对 id = 2 的张三进行信息的更新:

    表数据

    针对上图,我想提个问题。分别在 RC(读提交)以及 RR(可重复读)隔离级别下,T4 和 T5 时间点的查询 age 值分别是多少呢?T4 更新的值又是多少呢?思考片刻,相信大家都有自己的答案。答案在文末,希望大家能带着自己的疑问继续读下去。

    2.3.2 RR(可重复读)下的结果

    RR 级别下,查询只承认在事务启动前就已经提交完成的数据,一旦启动事务就会建视图。所以使用 start transaction with consistent snapshot 命令,马上就会建视图

    现在假设:

    在这种隔离级别下,他们创建视图的时刻如下:

    RR级别结果

    根据上图得,事务 A 的视图数组是[2,3];事务 B 的视图数组是 [2,3,4];事务 C 的视图数组是[2,3,4,5]。分析一波:

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

    其实视图是否可见主要看创建视图和提交的时机,总结下规律:

    2.3.2.1 快照读和当前读

    事务 B 的 update 语句,如果按照上图的一致性读,好像结果不大对?

    如下图周明,B 的视图数组是先生成的,之后事务 C 才提交。那就应该看不见 C 修改的 age = 23 呀?最后 B 怎么得出 24 了?

    更新逻辑

    没错,如果 B 在更新之前执行查询语句,那返回的结果肯定是 age = 22。问题是更新就不能在历史版本更新了呀,否则 C 的更新不就丢失了?

    所以,更新有个规则:更新数据都是先读后写(读是更新语句执行,不是我们手动执行),读的就是当前版本的值,叫当前读;而我们普通的查询语句就叫快照读

    因此,在更新时,当前读读到的是 age = 23,更新之后就成 24 啦。

    2.3.2.2 select 当前读

    除了更新语句,查询语句如果加锁也是当前读。如果把事务 A 的查询语句 select age from t where id = 2 改一下,加上锁(lock in mode 或者 for update),也都可以得到当前版本 4 返回的 age = 24

    下面就是加了锁的 select 语句:

    select age from t where id = 2 lock in mode;
     select age from t where id = 2 for update;

    2.3.2.3 事务 C 不马上提交

    假设事务 C 不马上提交,但是 age = 23 版本已生成。事务 B 的更新将会怎么走呢?

    事务 C 不马上提交

    事务 C 还没提交,写锁还没释放,但是事务 B 的更新必须要当前读且必须加锁。所以事务 B 就阻塞了,必须等到事务 C 提交,释放锁才能继续当前的读。

    被事务 C 锁住

    2.3.3 RC(读提交)下的结果

    在读提交隔离级别下,查询只承认在语句启动前就已经提交完成的数据;每一个语句执行之前都会重新算出一个新的视图

    注意:在上图的表格中用于启动事务的是 start transaction with consistent snapshot 命令,它会创建一个持续整个事务的视图。所以,在 RC 级别下,这命令其实不起作用。等效于普通的 start transaction(在执行 sql 语句之前才算是启动了事务)。所以,事务 B 的更新其实是在事务 C 之后的,它还没真正启动事务,而 C 已提交

    现在假设:

    在这种隔离级别下,他们创建视图的时刻如下:

    RC级别结果

    根据上图得,事务 A 的视图数组是[2,3,4],但它的高水位是 6或者更大(已创建事务 ID + 1);事务 B 的视图数组是 [2,4];事务 C 的视图数组是 [2,5]。分析一波:

    03巨人的肩膀

    04 总结

    本文详细聊了事务的方方面面,比如:四大特性、隔离级别、解决的并发问题、如何设置、查看隔离级别、如何启动事务等。除此以外,还深入了解了 RR 和 RC 两个级别的隔离是怎么实现的?包括详解 MVCC、undo log 和 read view 是怎么配合实现 MVCC 的。最后还聊了快照读、当前读等等。可以说,事务相关的知识点都在这了。看完这一篇还不懂的话,你来捶我呀!

    【相关推荐:mysql视频教程

    以上就是一文详解MySQL中的事务和 MVCC 原理的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:segmentfault,如有侵犯,请联系admin@php.cn删除
    专题推荐:mysql 事务 MVCC原理
    上一篇:MySQL详细解析之Clone插件 下一篇:mysql报错注入是什么
    VIP课程(WEB全栈开发)

    相关文章推荐

    • 【腾讯云】年中优惠,「专享618元」优惠券!• MySQL数据库基础知识点储备(整理总结)• MySQL你必须要了解存储引擎• 归纳详解MySQL知识点之表结构• 浅析MySQL中的事务隔离级别,聊聊其实现原理• MySQL详细解析之Clone插件
    1/1

    PHP中文网