从头学SQL(六):MySQL事务(ACID实现原理以及四大隔离级别的实现原理)

事务简述

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
事务用来管理 insert,update,delete 语句

ACID

一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

  1. 原子性: 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  2. 一致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  3. 隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。 事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read
    committed)、可重复读(repeatable read)和串行化(Serializable)。
  4. 持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务控制语句

  • BEGIN 或 START TRANSACTION 显式地开启一个事务;
  • COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;
  • ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
  • SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个保存点,一个事务中可以有多个 SAVEPOINT;
  • RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
  • ROLLBACK TO identifier 把事务回滚到标记点;
  • SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。

MYSQL 事务处理主要有两种方法

1、用 BEGIN, ROLLBACK, COMMIT来实现

BEGIN 开始一个事务
ROLLBACK 事务回滚
COMMIT 事务提交

2、直接用 SET 来改变 MySQL 的自动提交模式:

SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交

参考链接:https://www.runoob.com/mysql/mysql-transaction.html

实现ACID的原理

参考链接:https://www.cnblogs.com/superchong/p/10847966.html
参考链接:https://blog.csdn.net/Macky_He/article/details/99407383

如何实现原子性?

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。

如果事务中一个 sql 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

实现原理:undo log

在说明原子性原理之前,首先介绍一下 MySQL 的事务日志。MySQL 的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等。

此外 InnoDB 存储引擎还提供了两种事务日志:

redo log(重做日志)和undo log(回滚日志),其中 redo log 用于保证事务持久性;undo log
则是事务原子性和隔离性实现的基础。

实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的 sql 语句。

InnoDB 实现回滚,靠的是 undo log:

当事务对数据库进行修改时,InnoDB 会生成对应的 undo log。如果事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。undo log 属于逻辑日志,它记录的是 sql 执行相关的信息。当发生回滚时,InnoDB 会根据 undo log 的内容做与之前相反的工作:

例如:
(1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据
(2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作
(3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作

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

如何实现一致性?

这个问题分为两个层面来说。

  • 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。

但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给B账户加钱,那一致性还是无法保证。因此,还必须从应用层角度考虑。

  • 从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!

如何实现持久性?

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

实现原理: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 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

于是,redo log 被引入来解决这个问题:

  • 当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。
  • 如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复。
  • redo log 采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。

引申问题

问题一:既然 redo log 也需要在事务提交时将日志写入磁盘,为什么它比直接将 Buffer Pool 中修改的数据写入磁盘(即刷脏)要快呢?

主要有以下两方面的原因:

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

问题二:redo log 与 binlog区别

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

  • 作用不同:
    redo log 是用于 crash recovery 的,保证 MySQL 宕机也不会影响持久性;binlog 是用于point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制。
  • 层次不同:
    redo log 是 InnoDB 存储引擎实现的,而 binlog 是 MySQL 的服务器层(可以参考文章前面对 MySQL 逻辑架构的介绍)实现的,同时支持 InnoDB 和其他存储引擎。
  • 内容不同:
    redo log 是物理日志,内容基于磁盘的 Page。binlog 是逻辑日志,内容是一条条 sql。
  • 写入时机不同:
    redo log 的写入时机相对多元。前面曾提到,当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认情况下的策略,修改 innodb_flush_log_at_trx_commit 参数可以改变该策略,但事务的持久性将无法保证。除了事务提交时,还有其他刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不一定要等到 commit 时刷盘,commit 速度大大加快。
    binlog 在事务提交时写入。

如何实现隔离性?

与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。

隔离性是指事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作)。

那么隔离性的探讨,主要可以分为两个方面:

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

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

锁机制的基本原理可以概括为:

  1. 事务在修改数据之前,需要先获得相应的锁。
  2. 获得锁之后,事务便可以修改数据。
  3. 该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

行锁与表锁:

  • 按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。
  • 表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。
  • 但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。
  • MySQL 中不同的存储引擎支持的锁是不一样的,例如 MyIsam 只支持表锁,而 InnoDB 同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。

下面讨论写操作对读操作的影响。

脏读、不可重复读和幻读

首先来看并发情况下,读操作可能存在的三类问题。

①脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。
在这里插入图片描述

②不可重复读:在事务 A 中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。
在这里插入图片描述

③幻读:在事务 A 中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。
在这里插入图片描述

脏读与不可重复读的区别在于:

前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

不可重复读与幻读的区别可以通俗的理解为:

前者是数据变了,后者是数据的行数变了。

事务隔离级别

参考链接:https://www.cnblogs.com/superchong/p/10847966.html

sql 标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。

一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。

隔离级别与读问题的关系如下:
在这里插入图片描述

  • 可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。
  • 因此在大多数数据库系统中,默认的隔离级别是读已提交(如 Oracle)或可重复读(后文简称 RR)。
  • InnoDB 默认的隔离级别是 RR

MVCC

RR 解决脏读、不可重复读、幻读等问题,使用的是 MVCC:MVCC 全称 Multi-Version Concurrency Control,即多版本的并发控制协议

下面的例子很好的体现了 MVCC 的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在 T5 时刻,事务 A 和事务 C 可以读取到不同版本的数据。
在这里插入图片描述

  • MVCC 最大的优点是读不加锁,因此读写不冲突,并发性能好。
  • InnoDB 实现 MVCC,多个版本的数据可以共存,主要是依靠数据的隐藏列(也可以称之为标记位)和 undo log
  • 其中数据的隐藏列包括了该行数据的版本号、删除时间、指向 undo log 的指针等等。
  • 当读取数据时,MySQL 可以通过隐藏列判断是否需要回滚并找到回滚需要的 undo log,从而实现 MVCC

下面结合前文提到的几个问题分别说明。

①脏读

在这里插入图片描述

  • 当事务 A 在 T3 时间节点读取 zhangsan 的余额时,会发现数据已被其他事务修改,且状态为未提交。
  • 此时事务 A 读取最新数据后,根据数据的 undo log 执行回滚操作,得到事务 B 修改前的数据,从而避免了脏读。

②不可重复读
在这里插入图片描述

  • 当事务 A 在 T2 节点第一次读取数据时,会记录该数据的版本号(数据的版本号是以 row 为单位记录的),假设版本号为 1;当事务 B 提交时,该行记录的版本号增加,假设版本号为 2。
  • 当事务 A 在 T5 再一次读取数据时,发现数据的版本号(2)大于第一次读取时记录的版本号(1),因此会根据 undo log 执行回滚操作,得到版本号为 1 时的数据,从而实现了可重复读。

③幻读

  • InnoDB 实现的 RR 通过 next-keylock 机制避免了幻读现象。
  • next-keylock 是行锁的一种,实现相当于 record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock 的功能),还会锁定一个范围(gap lock 的功能)。

当然,这里我们讨论的是不加锁读:此时的 next-key lock 并不是真的加锁,只是为读取的数据增加了标记(标记内容包括数据的版本号等);准确起见姑且称之为类 next-key lock 机制。

还是以前面的例子来说明:
在这里插入图片描述
当事务 A 在 T2 节点第一次读取 :版本0

这样当 T5 时刻再次读取:版本 0

不过需要说明的是,RR 虽然避免了幻读问题,但是毕竟不是 Serializable,不能保证完全的隔离。

下面是一个例子,大家可以自己验证一下:
在这里插入图片描述
T2时刻事务A查询的版本为版本0;
T3时刻事务B修改后的版本为版本1;
T5时刻修改(并不是查询)了两条数据(数据笔数发生变化)。

总结

本文介绍了事务的ACID概念及原理,最后简要说明了四大隔离级别以及实现方式。文中内容均来自网络(链接已给出,建议去看原文),由本人整理所得,希望和小伙伴们一起学习。

引用

MYSQL事务
mysql中事务ACID实现原理
mysql中事务及ACID实现原理

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值