侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

深入学习MySQL事务:ACID特性的实现原理

再见理想
2022-05-26 / 0 评论 / 0 点赞 / 381 阅读 / 6,295 字

一,前言

事务是MySQL等关系型数据库区别于NoSQL的重要方面,是保证数据一致性的重要手段。本文将首先介绍MySQL事务相关的基础概念,然后介绍事务的ACID特性,并分析其实现原理。

二,基础概念

1,逻辑架构和存储引擎

mysql_howtowork

如上图所示,MySQL服务器逻辑架构从上往下可以分为三层:

  1. 第一层:处理客户端连接、授权认证等。
  2. 第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。
  3. 第三层:存储引擎,负责MySQL中数据的存储和提取。MySQL中服务器层不管理事务,事务是由存储引擎实现的MySQL支持事务的存储引擎有 InnoDB,InnoDB 的使用最为广泛;其他存储引擎不支持事务,如 MyIsam。

后文中描述的内容都是基于 InnoDB。

2,提交和回滚

典型的MySQL事务是如下操作的:

start transaction;
……  #一条或多条sql语句
commit;

其中 start transaction 标识事务开始,commit 提交事务,将执行结果写入到数据库。如果 sql 语句执行出现问题,会调用 rollback,回滚所有已经执行成功的sql语句。当然,也可以在事务中直接使用 rollback 语句进行回滚。

自动提交

MySQL 中默认采用的是自动提交(autocommit)模式。在自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个 sql语句都会被当做一个事务执行提交操作。

可以通过 set autocommit = 0; 命令关闭 autocommit;需要注意的是,autocommit 参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。

特殊操作

在 MySQL 中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行 commit 提交事务;如 DDL 语句(create table/drop table/alter/table)、lock tables语句等等。

3,ACID特性

ACID 是衡量事务的四个特性:

  • 1, 原子性(Atomicity,或称不可分割性)
  • 2, 一致性(Consistency)
  • 3, 隔离性(Isolation)
  • 4, 持久性(Durability)

按照严格的标准,只有同时满足 ACID 特性才是事务;但 MySQL 的 InnoDB 默认事务隔离级别是可重复读,不满足隔离性。因此与其说ACID 是事务必须满足的条件,不如说它们是衡量事务的四个维度。

4,事务并发问题

脏读:A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。
不可重复读:同样的 select 查询,但是结果不同,过程中有事务更新了原有的数据。
幻读:两次查询的结果数量不一样,过程中有事务新增或者是删除数据。

5,事务隔离级别

分别为读未提交,读已提交,可重复读,系列化。
读未提交:
其它事务都可以看到其他未提交事务的执行结果。
可能会产生脏读、不可重复读和幻读;

读已提交(RC):
其它事务只能看见已经提交事务的执行结果。
可能会产生不可重复读和幻读;

可重复读(RR,MySQL默认) :
多次读取同一范围的数据会返回第一次查询的快照,即使其他事务对该数据做了更新修改。事务在执行期间看到的数据前后必须是一致的。
但如果其他事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,这就是幻读。
可能会产生幻读。

串行化:
最高的隔离级别,通过加锁强制事务串行,避免了前面说的幻读问题。

6,RC、RR隔离级别实现原理

读已提交(RC)和可重复读(RR) 的读操作和写操作原理都一样!都是使用 MVCC 机制来实现的,而不同之处在于:行记录对于当前事务的可见性。

写操作:加行级锁。事务开始后,会在 undolog 日志中写入修改记录,数据行中的隐藏列 DATA_POLL_PTR 存储指向该行的 undolog 记录的指针。
读操作:不加锁。在读取时,如果该行被其它事务锁定,则顺着隐藏列 DATA_POLL_PTR 指针,找到上一个有效的历史记录(有效的记录:该记录对当前事务可见,且DELETE_BIT=0)。

提交读和可重复读都是使用 MVCC 机制来实现的,但是实现过程略微有一些不同,因为这些差别可以达到不同的隔离级别:
① 在 innodb 中的 RR 级别, 只有事务在 begin 之后,执行第一条 select 时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务结束前都是使用的这个快照,不会重新创建,直到事务结束。
② 在 innodb 中的 RC 级别, 事务在 begin 之后,执行每条 select 语句时,都会创建一个快照。


下面介绍ACID特性的实现原理:

三,原子性

1,定义

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

2,实现原理:undo log

在说明原子性原理之前,首先介绍一下 MySQL 的事务日志。MySQL 的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外 InnoDB 存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log 用于保证事务持久性;undo log 则是事务原子性和隔离性实现的基础。

下面说回undo log实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。

以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。


四,持久性

1,定义

持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

2,实现原理:redo log

redo log 和 undo log 都属于 InnoDB 的事务日志。下面先聊一下 redo log 存在的背景。

InnoDB 作为 MySQL 的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘 IO,效率会很低。为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:

当从数据库读取数据时,会首先从 Buffer Pool 中读取,如果 Buffer Pool 中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。


问题一:
Buffer Pool 的使用大大提高了读写数据的效率,但是也带来了新的问题:如果 MySQL 宕机,而此时Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

于是,redolog 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作。

当事务提交时,会对 redolog 进行刷盘。如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复。
redolog 采用的是 WAL(预写式日志,二阶段提交),所有修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。


问题二:
既然 redolog 也需要在事务提交时将日志写入磁盘,为什么它比直接将 Buffer Pool 中修改的数据写入磁盘(刷脏)要快呢?主要有以下两方面的原因:

  1. 刷脏是随机IO,因为每次修改的数据位置随机,但写 redolog 是追加操作,属于顺序IO
  2. 刷脏是以数据页为单位的,MySQL默认页大小是16KB,一个 Page 上一个小修改都要整页写入;而 redolog 中只包含真正需要写入的部分,无效IO大大减少。

2,redo log与binlog

我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

(1)作用不同:redo log 是用于 crash recovery 的,保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

(2)层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。

(3)内容不同redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

  • 前面曾提到:当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit 参数可以改变该策略,但事务的持久性将无法保证。
  • 除了事务提交时,还有其他刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不一定要等到 commit 时刷盘,commit 速度大大加快。

五,隔离性

1,定义

与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

  • (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
  • (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

2,锁机制

首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB 通过锁机制来保证这一点。

锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

行锁与表锁

按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;

行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。

MySQL中不同的存储引擎支持的锁是不一样的,例如 MyIsam 只支持表锁,而 InnoDB 同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。


5,MVCC

RR解决脏读、不可重复读、幻读等问题,使用的是MVCC:MVCC 全称 Multi-Version Concurrency Control,即多版本的并发控制协议。 MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

MySQL事务隔离级别有四种,其中 RC 和 RR 才使用到 MVCC 机制。因为在读未提交(RU)级别下是直接返回记录上的最新值,Serializable 级别下则会对所有读取的行都加锁。

MVCC参考文章
MySQL MVCC 和锁机制
什么是MySQL的MVCC机制


5.1,什么是Read View

Read View 是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID。主要是用来做可见性判断,把它比作条件用来判断当前事务能够看到哪个版本的数据。
Read View 有几个重要的参数:

  • m_ids:表示生成ReadView时,当前系统正在活跃的读写事务的事务Id列表。
  • min_trx_id:表示生成ReadView时,当前系统中活跃的读写事务的最小事务Id。
  • max_trx_id:表示生成ReadView时,当前时间戳InnoDB将在下一次分配的事务id。
  • creator_trx_id:当前事务id。

5.2,隐藏列

InnoDB存储引擎中,它的聚簇索引记录中都包含两个必要的隐藏列,分别是:

  • trx_id:事务Id,每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列。
  • roll_pointer:回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo log 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

5.3,事物链

每次对记录进行修改时,都会记录一条 undo log 信息,每一条 undo log 信息都会有一个 roll_pointer 属性,可以将这些undo日志都连起来,串成一个链表。事务链如下图一样:


5.4,原理

RC 和 RR 隔离级别的实现就是通过版本控制来完成,核心处理逻辑就是判断所有版本中哪个版本是当前事务可见的处理。MVCC 就是通过 undo log 版本链 + ReadView 实现的一套并发读取的机制

判断的逻辑如下:

  • 如果被访问版本的trx_id属性值小于ReadView的最小事务Id,表示该版本的事务在生成 ReadView 前已经提交,所以该版本可以被当前事务访问;
  • 如果被访问版本的trx_id属性值大于ReadView的最大事务Id,表示该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问;
  • 如果被访问版本的trx_id属性值在m_ids列表最小事务Id和最大事务Id之间,那就需要判断一下 trx_id 属性值是不是包含在 m_ids 列表中,如果包含的话,说明创建 ReadView 时生成该版本的事务还是活跃的,所以该版本不可以访问;如果不包含的话,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问;

RR 和 RC 实现 MVCC 的区别?

在读已提交 RC 隔离级别下,每次执行 select 前都会重新建立一个新的 ReadView,因此如果事务A第一次 select 之后,事务B对数据进行了修改并提交,那么事务A第二次 select 时会重新建立新的 ReadView,因此事务B的修改对事务A是可见的。因此RC隔离级别可以避免脏读,但是无法避免不可重复读和幻读。

在可重复读 RR 隔离级别下,在事务开始后第一次执行 select 前创建 ReadView 读视图,直到事务提交都不会再创建。别的事务未提交、已提交、新插入的修改都读取不到,因此解决了 脏读、不可重复读、幻读 的问题。


5.5,加锁读 与 next-key lock

按照是否加锁,MySQL的读可以分为两种:

一种是非加锁读,也称作快照读、一致性读,使用普通的 select 语句,,这种就是读取 undo 版本链上的一个快照版本,这种情况下使用 MVCC 避免了脏读、不可重复读、幻读,保证了隔离性。

另一种是加锁读,查询语句有所不同,如下所示:

#共享锁读取
select...lock in share mode
#排它锁读取
select...for update

加锁读在查询时会对查询的数据加锁(共享锁或排它锁)。由于锁的特性,当某事务对数据进行加锁读后,其他事务无法对数据进行写操作,因此可以避免脏读和不可重复读。

而避免幻读,则需要通过 next-key lock。next-key lock是行锁的一种,实现相当于记录锁 + 间隙锁;其特点是不仅会锁住记录本身,还会锁定一个范围。因此,加锁读同样可以避免脏读、不可重复读和幻读,保证隔离性。


5.6,RR 隔离级别下 MVCC 解决幻读了吗?

首先可以明确的是,MVCC 在快照读的情况下可以解决幻读问题,但是在当前读的情况下是不能解决幻读的。

快照读:也叫普通读,读取的是记录数据的可见版本,不加锁的普通select语句都是快照读。
当前读:读取的是记录数据的最新版本,并且需要先获取对应记录的锁。

快照读情况:
通过 MVCC 的 Read View 对版本快照中各个版本链中的数据进行可见性判断,读取相应的数据版本。即使另一事务新插入了数据,由于已经生成了版本快照,也不会影响 Read View 的可见性规则判读,所以在快照读的情况下,使用MVCC不会产生幻读问题。

当前读情况:
当前读下,每次读取的记录数据都是最新版本。所有 MVCC 在当前读的情况下,不能解决幻读问题。这时,是如何避免幻读情况产生的?
RR隔离级别下,会使用 next-key locks 记录锁+间隙锁方式,锁住索引记录之间的范围,避免幻读情况发生。


5.7,MVCC 总结

要理解MVCC机制,关键在于要理解ReadView、隐藏列、事务链三者在其中的作用。
还有就是只有 RC 和 RR 的隔离级别才会使用 MVCC 机制,两者最大的区别在于生成 ReadView 的时机的不同,RC 级别生成 ReadView 的时机是每次查询都会生成新的 ReadView,而 RR 级别是在当前事务第一次查询时生成,并且生成的ReadView会一直沿用到事务提交为止,保证可重复读。


六,一致性

1,基本概念

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。

2,实现

可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。

实现一致性的措施包括:

  • 1, 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
  • 2, 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
  • 3, 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

七,总结

下面总结一下ACID特性及其实现原理:

  • 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于 undolog

  • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于 redolog

  • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是可重复读 RR,RR的实现主要基于锁机制、MVCC(包括数据的隐藏列、基于undolog的版本链、ReadView)。

  • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障。


文章参考:

你说熟悉MySQL事务,那来谈谈事务的实现原理吧!

MySQL MVCC 和锁机制

0

评论区