MySQL 事务:ACID 特性的实现原理知多少,java 教学视频百度云
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 中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
1、刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是追加操作,属于顺序 IO。
2、刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16KB,一个 Page 上一个小修改都要整页写入;而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少。
3. 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。
写入时机不同:binlog 在事务提交时写入;redo log 的写入时机相对多元:
前面曾提到:当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认情况下的策略,修改 innodb_flush_log_at_trx_commit 参数可以改变该策略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不一定要等到 commit 时刷盘,commit 速度大大加快。
=========================================================================
与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的 Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个方面:
(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
(一个事务)写操作对(另一个事务)读操作的影响:MVCC 保证隔离性
首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB 通过锁机制来保证这一点。
锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
行锁与表锁
按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL 中不同的存储引擎支持的锁是不一样的,例如 MyIsam 只支持表锁,而 InnoDB 同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
如何查看锁信息
有多种方法可以查看 InnoDB 中锁的情况,例如:
select * from information_schema.innodb_locks; #锁的概况
show engine innodb status; #InnoDB 整体状态,其中包括锁的情况
下面来看一个例子:
#在事务 A 中执行:start transaction;update account SET balance = 1000 where id = 1;#在事务 B 中执行:start transaction;update account SET balance = 2000 where id = 1;
此时查看锁的情况:
show engine innodb status 查看锁相关的部分:
通过上述命令可以查看事务 24052 和 24053 占用锁的情况;其中 lock_type 为 RECORD,代表锁为行锁(记录锁);lock_mode 为 X,代表排它锁(写锁)。
除了排它锁(写锁)之外,MySQL 中还有共享锁(读锁)的概念。由于本文重点是 MySQL 事务的实现原理,因此对锁的介绍到此为止,后续会专门写文章分析 MySQL 中不同锁的区别、使用场景等,欢迎关注。
介绍完写操作之间的相互影响,下面讨论写操作对读操作的影响。
首先来看并发情况下,读操作可能存在的三类问题:
**脏读:**当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。举例如下(以账户余额表为例):
**不可重复读:**在事务 A 中先后两次读取同一个数据,两次
读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。举例如下:
**幻读:**在事务 A 中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。举例如下:
SQL 标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:
在实际应用中,读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。因此在大多数数据库系统中,默认的隔离级别是读已提交(如 Oracle)或可重复读(后文简称 RR)。
可以通过如下两个命令分别查看全局隔离级别和本次会话的隔离级别:
InnoDB 默认的隔离级别是 RR,后文会重点介绍 RR。需要注意的是,在 SQL 标准中,RR 是无法避免幻读问题的,但是 InnoDB 实现的 RR 避免了幻读问题。
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-key lock 机制避免了幻读现象。
next-key lock 是行锁的一种,实现相当于 record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock 的功能),还会锁定一个范围(gap lock 的功能)。当然,这里我们讨论的是不加锁读:此时的 next-key lock 并不是真的加锁,只是为读取的数据增加了标记(标记内容包括数据的版本号等);准确起见姑且称之为类 next-key lock 机制。还是以前面的例子来说明:
当事务 A 在 T2 节点第一次读取 0<id<5 数据时,标记的不只是 id=1 的数据,而是将范围(0,5)进行了标记,这样当 T5 时刻再次读取 0<id<5 数据时,便可以发现 id=4 的数据比之前标记的版本号更高,此时再结合 undo log 执行回滚操作,避免了幻读。
概括来说,InnoDB 实现的 RR,通过锁机制、数据的隐藏列、undo log 和类 next-key lock,实现了一定程度的隔离性,可以满足大多数场景的需要。不过需要说明的是,RR 虽然避免了幻读问题,但是毕竟不是 Serializable,不能保证完全的隔离,下面是一个例子,大家可以自己验证一下。
=========================================================================
##1. 基本概念
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。
实现一致性的措施包括:
保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致
评论