mysql-事务和ACID

基础概念

事务概念及逻辑架构

事务是访问和更新数据库的程序的执行单元。事务可能包含一个或多个SQL,这些SQL要么都执行成功,要么都执行失败。MySQL支持事务。

MySQL服务器的逻辑架构可以分为三层:

  • 第一层,处理客户端连接、授权管理
  • 第二层,服务器层,语句的解析、优化、缓存以及内置函数、存储过程的实现
  • 第三层,存储引擎层,负责数据的存储和读取。MySQL中的事务是由存储引擎实现的。其中应该最广泛的就是InnoDB引擎,支持MySQL事务。

事务提交和回滚

典型的MySQL事务操作如下:

1
2
3
start transaction;
...#一条或多条SQL
commit;

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

自动提交

MySQL默认采用自动提交autocommit的模式。

1
show variables like '%autocommit%';

在自动提交的模式下,如果没有start transaction显式的开启一个事务,每个SQL也会被当做一个事务被提交。

可以通过如下方式关闭自动提交:

1
set autocommit = 0;

这里需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会影响其他连接。

特殊操作

在MySQL中,有一些特殊命令,会强制提交事务,如DDL(create table、drop table、alter table)。不过常用的 select、update、insert、delete都不会强制提交事务。

ACID特性

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

  • A 原子性
  • C 一致性
  • I 隔离性
  • D 持久性

因为能满足ACID的事务少之又少,因此与其说ACID是事务必须满足的条件,倒不如说是衡量事务的四个维度。

原子性

原子性指的是一个事务是一个不可分割的整体,要么都做,要么都不做。如果事务中一个SQL失败,则事务回滚到事务前的状态。

那么InnoDB是如何保证事务的原子性的?实现的原理是:undo log

InnoDB提供了两种事务日志:redo log(重做日志)undo log(回滚日志)。其中redo log用来保证事务的持久性,undo log则是事务原子性和隔离性实现的基础。

下面来说undo log

实现原子性的关键时,当事务回滚时撤销所有已执行成功的SQL。InnoDB是这么做的,当事务对数据库进行修改时,InnoDB会生成对应的undo log;若事务执行失败或执行了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到事务前的状态。

undo log数据逻辑日志,它记录的是sql执行的相关信息。当发生回滚时,InnoDB会根据undo log做相反的操作。对于insert,则执行delete

持久性

持久性是指事务一旦提交,对数据库的改变就是永久的。

前面说过,InnoDB使用redo log来保证事务的持久性。

InnoDB作为MySQL的存储引擎,数据是存储在磁盘的,但如果每次读写都需要磁盘IO,效率会很低。所以,InnoDB为此提供了一个缓存Buffer poolBuffer pool包含了部分磁盘数据的映射,作为访问数据库的缓冲。当从数据库读取数据时先从Buffer pool中读取,读取不到再从磁盘中读取,然后放到Buffer pool。当向数据库写入时,先写入到Buffer poolBuffer pool中修改的数据会定期刷到磁盘中(刷脏)。

Buffer pool为读写带来了极大的便利,但也带来了新的问题,如果MySQL宕机,而此时,Buffer pool中的数据还没有刷到磁盘中,就会导致这部分数据丢失,无法保证持久化。

于是,redo log被用来解决这个问题,当数据修改时,除了修改Buffer pool,还会在redo log中记录这次修改;当事务提交时,会对redo log进行刷盘。如果MySQL宕机,重启时则可以从redo log中去恢复。redo log采用WAL(Write Ahead Logging)技术,所有修改先写日志,再写入到Buffer pool,保证了数据不会因为宕机而丢失,从而保证了持久性。

既然redo log也需要在事务提交时将日志写入磁盘,那为什么会比直接将Buffer pool中的数据直接写入磁盘快呢?

  1. 刷脏是随机IO,因为每次修改的数据位置是随机的,但是redo log是循环写,属于顺序IO。
  2. 刷脏是以数据页为单位,默认页大小是16kb,有一个小修改都需要整个页写入。而redo log中只写入真正修改的部分。

redo log 和 bin log

我们知道MySQL的bin log也可以记录写操作并且恢复数据,但和redo log有着根本的不同:

  1. 作用不同:redo log重做日志是用于 crash recovery的,保证MySQL宕机也不会影响持久性;binlog归档日志是用来保证服务器是基于时间点恢复数据的,此外还用于主从复制。
  2. 层次不同:redo log位于存储引擎层,而binlog是MySQL服务器层。
  3. 内容不同:redo log是物理日志,记录的是“在某个数据页上做了什么修改”,内容基于磁盘页。binlog是二进制日志,可能基于SQL、或数据行,或者二者结合。
  4. 写入方式不同:redo log是循环写的;binlog时追加写的,binlog文件大到一定程度后会切换到下一个文件,不会覆盖之前文件。
  5. 写入时机不同:binlog在事务提交时写入,redo log写入时机相对多元。

两阶段提交

目的:是为了redo log和binlog两份日志之间逻辑一致。

binlog记录了所有逻辑操作,并且采用了追加写的方式。因此不可能让binlog无限制的增大。这里的一个措施是,定期做整库备份,如半月一备份,同时系统还会保留最近这半个月的binlog。

如果需要恢复到指定的某一秒,如某天下午两点发现中午十二点有一次误删表,需要找回数据,可以这么做:

  • 首先,找对最近一次的全量备份数据,从这个备份库恢复到临时库
  • 然后,从备份时间开始,将备份的数据取出来,重放到中午误删表的时刻。
  • 然后临时库和误删之前的库一样了,取出表数据,按需要进行恢复。

为什么需要两阶段?

对于一条更新语句,假设在第一个日志写完后,第二日志还没写完时发生了crash。会出现什么情况?

  1. 先写redo log后写binlog。假设redo log 写完,binlog还未写完,MySQL进程异常重启,redo log已经写完,所以仍然能够恢复数据。但是binlog就缺少了这条更新语句。若是以binlog恢复临时库的时候,就会缺少一次更新
  2. 先写binlog后写redo log。在binlog写完后发生了crash。由于redo log还没写,恢复后这个事务无效。但是通过binlog恢复出来的数据却是有这条数据。

可以看到若不使用两阶段提交,通过redo log和binlog恢复的数据有可能会不一致。

简单来说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交让这两个状态保持逻辑上的一致。

隔离性

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

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

简单起见,我们只考虑最简单的读和写操作。那么可以分为两个方面:

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

锁机制

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

表锁和行锁

表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。 MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。

下面来考虑写操作对读的影响:

在并发情况下,读操作可能存在三种问题:脏读、幻读、不可重复读。

脏读

当前事务A中可以读取到其他事务未提交的数据。以账户余额表举例:

时间 事务A 事务 B
T1 开始事务 开始事务
T2 修改张三的余额,将余额又100改为200
T3 查询zhangsan的余额,结果为200【脏读】
T4 提交事务

不可重复读

在事务A中先后两次读取同一个数据,两次数据结果不一致,这种现象称为不可重复读。脏读和不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

时间 事务A 事务B
T1 开始事务 开始事务
T2 查询zhangsan的余额,结果为100
T3 修改zhangsan余额为200
T4 提交事务
T5 查询zhangsan余额,结果为200

幻读

在事务A按照某个条件查询了两次数据库,两次查询结果不同。不可重复读和幻读的区别可以理解为:前者是数据变了,后者是数据行变了。

时间 事务A 事务B
T1 开始事务 开始事务
T2 查询 0 < id < 5的所有用户的余额
zhangsan 100 (id = 1)
T3 账户余额表插入lisi 200 (id = 2)
T4 提交事务
T5 查询 0 < id < 5的所有用户的余额
zhangsan 100 (id = 1)
lisi 200 (id = 2)

事务隔离级别

SQL标准定义了4种隔离级别:并规定了各种隔离级别可以解决上面哪些问题。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。

隔离级别 脏读 不可重复读 幻读
读未提交RU 可能 可能 可能
读已提交RC 不可能 可能 可能
可重复读RR 不可能 不可能 可能
可串行化 不可能 不可能 不可能

实际应用中,读未提交和可串行化很少用到。在大多数数据库中,默认的隔离级别是读已提交或可重复读。

InnoDB默认的隔离级别是RR。RR无法避免幻读的问题,但是InnoDB实现RR避免了幻读问题。

MVCC

RR解决脏读、不可重复读、幻读等问题,使用的是MVCC,多版本并发控制协议。

在同一时刻,不同事务读取到的数据可能是不同的(即多版本)—— 在 T5时刻,事务A和事务C可以读取到不同版本的数据。

时间 事务A 事务B 事务C
T1 开始事务 开始事务 开始事务
T2 查询zhangsan的余额为100
T3 修改zhangsan余额为200
T4 提交事务
T5 查询zhangsan余额为100 查询zhangsan余额为200

InnoDB的RR,通过undo log,实现了一定程度的隔离性,可以满足大部分需求。RR虽然避免了幻读问题,但不能保证完全隔离。

时间 事务A 事务B
T1 开始事务 开始事务
T2 查询 0 < id < 5的所有用户的余额
zhangsan:100(id=1)
T3 账户余额插入
lisi:200(id=2)
T4 提交事务
T5 修改0<id<5的所有用户的余额为300
T6 提交事务

提交事务后,zhangsan和lisi的余额都变为300。

一致性

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

前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。

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

0%