自学内容网 自学内容网

MySQL 事务

前置知识

MySQL InnoDB 引擎中的 快照读、当前读

  • 快照读(普通select语句):不加锁的select操作,非阻塞读。这种方式读取的数据不一定是最新的数据。
  • 当前读select ... for update):即读取的时候会加锁,保证其他事务不能修改当前记录。这种方式读取的记录是最新版本的数据

MySQL 事务

参考:

  1. https://www.xiaolincoding.com/mysql/transaction/mvcc.html
  2. https://javaguide.cn/database/mysql/innodb-implementation-of-mvcc.html
  3. https://www.cnblogs.com/qdhxhz/p/15750866.html

事务的特性ACID

事务是由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的,MyISAM 引擎不支持事务。

如果要实现事务,必须要遵守 4 个特性,分别如下:

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成。如果在执行过程中发生错误,会回滚的事务开始的状态。
  • 一致性(Consistency):事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。即数据的总额保持不变(A向B转账,转账完成二者账户数据总额应该是不变的)
  • 隔离性(Isolation):多个事务同时对相同数据操作时,不会相互干扰。每个事务都有一个完整的数据空间。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

InnoDB 引擎通过什么技术来保证事务的这四个特性?

  • 保证事务的持久性:是通过**redo log (重做日志)**来保证的;
  • 保证事务的原子性:是通过 undo log(回滚日志) 来保证的;
  • 保证事务的隔离性:是通过 MVCC(多版本并发控制)锁机制来保证的;
  • 保证事务的一致性:是通过持久性+原子性+隔离性来保证;(即 一致性 是最终目的)

并发事务会引起的问题:脏读、幻读、不可重复读

MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。此时就可能出现**脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)**的问题。

1、脏读(dirty read):一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象

读取到了其他事务还没有提交的数据。由于这些数据可能会因为另一个事务的回滚而被撤销,因此被读取的数据是“脏”的,即它们不是最终的数据。

2、不可重复读(non-repeatable read):在一个事务中多次读取同一个数据,但是出现前后两次读取到的数据不一样的情况,就说明发生了**「不可重复读」现象**。

这种现象发生场景:在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读。

3、幻读(phantom read):在一个事务中,多次查询某个符合查询条件的「记录数量」,如果出现前后查询到的 记录数量 不一样,则说明发生了幻读。

即,当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Rows)。

小结

上面的在并发的事务中出现的问题,严重程度如下:

在这里插入图片描述

解决 脏读、幻读、不可重复读

上面说道,多个事务并发执行会出现 脏读、幻读、不可重复读 问题。SQL 标准提出了 事务隔离级别 来规避这些问题的出现。

  • 读取未提交(read uncommitted):指一个事务还没提交时,它做的变更可以被其他事务看到;
  • 读取已提交(read committed):指一个事务提交之后,它做的变更才能被其他事务看到;
  • 可重复读(repeatable read):确保在同一个事务中多次读取同样记录的结果是一致的。【MySQL InnoDB引擎默认的 隔离级别
  • 串行化(serializable):对数据库中的记录加上读写锁,多个事务要操作该记录时,事务之间执行顺序会被串行化。

上面的隔离级别 按照隔离水平高低排序如下:(隔离水平越高、性能越差

在这里插入图片描述

单纯的「事务隔离级别」可解决的并发问题(脏读、幻读、不可重复读)

针对不同的隔离级别,并发事务时可能发生的问题也会不同。

在这里插入图片描述

  • 在「读取未提交」隔离级别下,不能解决 脏读、不可重复读、幻读;
  • 在「读取已提交」隔离级别下,不能解决 不可重复读、幻读,可以解决 脏读;
  • 在「可重复读」隔离级别下,不能解决 幻读,可以解决 脏读、不可重复读;
  • 在「串行化」隔离级别下,可以解决 脏读、不可重复读、幻读。

也就是说:

  1. 想要解决「脏读」,隔离级别只需提升到「读已提交」。
  2. 想要解决「不可重复读」,隔离级别只需提升到「可重复读」。
  3. 想要解决「幻读」,隔离级别需要提升到「串行化」。

解决并发问题(脏读、幻读、不可重复读)的实际方案

  1. 想要解决「脏读」,隔离级别只需提升到「读已提交」。
  2. 想要解决「不可重复读」,隔离级别只需提升到「可重复读」。

现在主要讨论「幻读」问题的解决,虽然 隔离级别「串行化」可以解决,但是这个隔离级别下,数据库性能大大下降。因此**「幻读」问题的解决方案一般采用**:

  1. 针对快照读(普通 select 语句)的场景下,解决幻读的方案:隔离级别设置为「可重复读」+ MVCC技术。「可重复读」隔离级别 可解决 脏读、不可重复读问题,MVCC技术解决 幻读问题。
  2. 针对当前读(select … for update 等语句)的场景下,解决幻读的方案:隔离级别设置为「可重复读」+ next-key lock(记录锁+间隙锁)。「可重复读」隔离级别 可解决 脏读、不可重复读问题,next-key lock(记录锁+间隙锁)解决 幻读问题。

问题

问:事务的4种隔离级别实现原理

  • 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;

  • 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;

  • 对于「读提交」和「可重复读」隔离级别的事务来说,通过 Read View 来实现的。二者区别在于:

    可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。

    • 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View;
    • 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View。

问:执行「开始事务」命令,就开始了事务吗?

答:执行「开始事务」命令,并不意味着启动了事务。需要分情况,在 MySQL 有两种开启事务的命令:

  1. 第一种:begin/start transaction 命令:执行命令后,当执行事务的第一条语句时,才会真正开启事务。
  2. 第二种:start transaction with consistent snapshot:执行该命令后,会立刻启动事务。

MVCC 机制

什么是 MVCC 机制?

MVCC(Multi-Version Concurrency Control)即多版本并发控制,是一种数据库管理系统中常用的并发控制机制,主要用于提高数据库的并发性能。

在并发场景下,读、写会出现三种情况:①读-读并发、读-写并发、③写-写并发

MySQL InnoDB 的 MVCC 就是用来实现并发情况下,读-写并发不用加锁的。注意,这里的 读-写 中的读 是指 快照读。它可以在 隔离级别为 可重复读(解决了 脏读、不可重复读问题) 的情况下,解决 幻读问题。

MVCC 实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读(快照读)写冲突,它的实现原理主要是依赖记录中的 隐式字段undo log日志Read View 来实现的。

隐藏字段

InnoDB 存储引擎为每行数据添加了三个 隐藏字段:

  • DB_TRX_ID(6字节):表示最后一次插入或更新该行的事务 id。

    此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除

  • DB_ROLL_PTR(7字节) 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空

  • DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

在这里插入图片描述

undo log 日志

undo log可以理解成回滚日志,它存储的是被事务修改之前的数据(它会随着事务的进行不断记录数据的历史版本,形成一个版本链,以便支持多版本并发控制(MVCC)等功能)。

InnoDB 存储引擎中 undo log 分为两种:insert undo logupdate undo logdelete undo log

由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。

  1. Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。

  2. Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。

  3. Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。

    删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。

举个例子:比如对一条记录进行了几次修改,该记录的隐藏字段DB_ROLL_PTR (回滚指针)会存储 该条记录的 undo log。如下:

在这里插入图片描述

Read View(读 视图)

ReadView 是事务在进行快照读的时候生成的记录快照, 可以帮助我们解决可见性问题的。如果一个事务要查询行记录,需要读取该记录的哪个版本呢? ReadView 就是来解决这个问题的。

ReadView是如何保证可见性判断的呢?我们先看看 ReadView 的几个重要属性:

  • trx_list:当前系统中那些活跃(未提交)的读写事务ID,存放在一个列表中。

  • up_limit_id:活跃事务列表trx_ids中最小的事务ID(如果trx_ids为空,则up_limit_id 为 low_limit_id)。

  • low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。

具体判断可见性的流程为:当前事务C中(假设事务C的ID为 C_ID),使用 快照读 的方式读取 记录行a(假设记录行a的隐藏字段trx_id记录的最近一次修改它的事务的ID为 a_ID

  1. 如果C_ID > a_ID:那么记录行a 的最新数据是 对事务C 可见的。那就意味着这个数据a在创建当前 Read View 的事务开始之前就已经被提交了。
  2. 如果C_ID < a_ID:那么记录行a 的最新数据是 对事务C 不可见的。那么会去 行记录a 的回滚链(其undo log)中,找到某条 日志中的事务ID 小于 C_ID才是可见的。
  3. 如果 a_ID在活跃事务列表中,此时对于事务C,行记录a的最新数据 是不可见的。那么会去 行记录a 的回滚链(其undo log)中,找到某条 日志中的事务ID 小于 C_ID才是可见的。
整体流程

了解了隐式字段,undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了。

在这里插入图片描述

流程其实就是事务开启,然后进行快照读 来读取行记录数据。剩下的就是看 当前事务的ID 和 行记录中的trx_id 比较。如果比较后不可见,则需要去 这个行记录a中的 undo log 日志中,查找可见的旧版本数据。


原文地址:https://blog.csdn.net/longlongqin/article/details/142433797

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!