事务是MySQL等关系型数据库区别于NoSQL的重要方面,是保证数据一致性的重要手段。本文将首先介绍MySQL事务相关的基础概念,然后介绍事务的ACID特性,并分析其实现原理。
学海无涯,文章疏漏在所难免,欢迎批评指正。
事务
维基百科说事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
很抽象的解释,那来通俗的解释下,就是包含一组操作语句,要不不执行,要不都执行,如果执行,就持久化了。比如小明和小亮账户都是1000元,小明转100元给小亮,小明余额变为900,小亮变为1100,两个操作都能完成,否则两个都取消。这两个操作整体是一个事务。
MySQL架构
如上图所示,MySQL服务器逻辑架构从上往下可以分为三层:
(1)第一层:处理客户端连接、授权认证等。
(2)第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。
(3)第三层:存储引擎,负责MySQL中数据的存储和提取。MySQL中服务器层不管理事务,事务是由存储引擎实现的。MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。
提交和回滚
开始一个事务
start transaction;
... sql操作语句
commit;
MySQL中默认采用的是自动提交,可以设置不自动提交
set autocommit = 0;
ACID
原子性
概念
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
原理: undo log
MySQL有很多种日志,错误日志、慢查询日志、查询日志,二进制日志等,InnoDB引擎还有undo log和redo log。
实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log,当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
持久性
概念
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
原理: redo log
MySQL数据时存放在磁盘中的,如果每次读写都需要磁盘io,效率太低了,InnoDB存储引擎提供了缓存Buffer Pool,读取时先从缓存中读取,如果没有就从磁盘读取放入缓存,写入也是先写入缓存,再定期刷入磁盘(这一过程称为刷脏)。
但是有个问题如果,宕机了数据就丢了,无法保证持久性,所以redo log就是为了解决这个问题。
当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到缓存。 保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
那redo log也是落盘,数据也是落盘,为什么日志要比数据快呢?原因有二:
- 刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
- 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
和二进制日志的区别
我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:
-
作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。
-
层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。
-
内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。
-
写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:
前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。 除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。
隔离性
概念
隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个方面:
- (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
- (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
隔离级别
隔离级别有四种:未提交读,已提交读,可重复读,串行读
- 未提交读是可以读到另一个事务还未commit的数据,会产生已提交读是可以读取
- 只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
- 可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
- 完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞,性能很差一般不用
一致性
概念
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
实现原理
前边所有特性的都是为了保证一致性,此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。 操作包括
- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致