MySQL事务ACID特性的实现原理

一、前提
在MySQL之binlog日志、undo日志、redo日志这篇文章中,我们简单介绍了MySql的三种日志。其中undo日志、redo日志,是实现MySQL事务的关键:redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。

二、数据库事务的特性

  • 原子性(A: Atomicity)
    所谓的原子性是指,整个事务中的所有操作,要么全部完成,要么全部回滚到事务初始状态,不存在有的成功有的失败这样的中间状态。事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
  • 一致性(C: Consistency)
    所谓的一致性指,一个事务执行之前、后数据库都必须处于一致性状态。即:
    事务执行成功,系统中所有变化将正确地应用,系统处于有效状态。
    事务执行出错,系统中的所有变化将自动地回滚,系统返回到原始状态。
    以转账为例,A有500元,B有300元,如果在一个事务里A成功转50元给B,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
  • 隔离性(I: Isolation)
    所谓的隔离性是指,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。即:
    在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自完整的数据空间。事务查看数据更新时,数据所处的状态 要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
  • 持久性(D: Durability)
    所谓的持久性是指,一旦事务提交完成,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也不会改变,或者说可以恢复到事务成功提交后的状态。

一、各个特性的实现原理

事务的四大特性中,原子性、隔离性、持久性都是为了一致性服务的。即,都是为了保证最后的数据一致的。

1. 原子性

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

过程:

  • 当事务对数据库进行修改时,InnoDB会生成对应的undo log;
  • 如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

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

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

2. 持久性

redo log是实现事务持久性的关键,是InnoDB特有的日志。

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被引入来解决这个问题:

  1. 当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;
  2. 当事务提交时,会调用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. 隔离性

  • 隔离性追求的是并发情形下事务之间互不干扰。InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制、数据的隐藏列、undo log和类next-key lock机制
  • 简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个方面:
    (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
    (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

3.1 锁机制

  • 隔离性要求同一时刻只能有一个事务对数据进行写操作,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;

此时查看锁的情况:

其中,lock_type为RECORD,代表锁为行锁(记录锁);lock_mode为X,代表排它锁(写锁)。

3.1.1事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:

  • 赃读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。举例如下(以账户余额表为例):
  • 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。举例如下:
  • 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。举例如下:

3.2 MVCC

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

发表评论