MySQL事务实现原理
参考博文:https://blog.csdn.net/qq_46312987/article/details/123941617
https://blog.csdn.net/binbigdata/article/details/82938781
什么是事务?
事务是访问和更新数据的程序执行单元,事务中可能含有一个或多个SQL语句,这些语句要么全部执行,要么都不执行
回顾MySQL的逻辑架构与存储引擎
如上图所示,MySQL服务器的逻辑架构从上到下分为三层:
- 第一层:处理客户端连接、授权认证等
- 第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。
- 第三层:存储引擎,负责MySQL中数据的存储与提取。MySQL中服务器层不管理事务,事务是由存储引擎实现的,MySQL支持事务的存储引擎有InnoDB,NDB Cluster等,使用最多的就是InnoDB,也是MySQL默认使用的存储引擎,其他引擎不支持事务,比如MyISAM,Memory等。
ACID
原子性(Atomicity)
一个事务可能包含多个SQL语句,原子性就是要保证这些SQL语句要么全部执行,要么全不执行。如果事务中的一条SQL执行失败,那么已经执行的语句也必须回滚。所以说,实现原子性的核心就在于如何实现回滚。
举个例子:
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。
一致性(Consistency)
事务在系统完整性中实施一致性,这通过保证系统的任何事务最后都处于有效状态来实现。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。因为事务开始时系统处于一致状态,所以现在系统仍然处于一致状态。
隔离性(Isolation)
并发事务之间互相影响的程度,比如一个事务会不会读取到另一个未提交的事务修改的数据。在事务并发操作时,可能出现的问题有:
- 脏读:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。
- 不可重复读:在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录,要避免这种情况,最简单的方法就是对要修改的记录加锁,这回导致锁竞争加剧,影响性能。另一种方法是通过MVCC可以在无锁的情况下,避免不可重复读。
- 幻读:在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。
事务的隔离级别从低到高有:
- Read Uncommitted:最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。
- Read Committed:只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。
- Repeated Read:在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。
- Serialization:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。
通常,在工程实践中,为了性能的考虑会对隔离性进行折中。
持久性(Persistence)
事务提交后,对系统的影响是永久的。
MySQL日志
在说明原子性原理之前,首先介绍一下MySQL的事务日志,MySQL的日志有很多种,比如二进制日志(binlog)、错误日志、查询日志、慢查询日志,此外InnoDB存储引擎还提供了两种事务日志,redo log(重做日志) 和 undo log(回滚日志), 其中redo log用于保证事务的持久性,而undo log则是事务原子性和隔离性实现的基础。
✳原子性实现关键 undo log
- 实现原子性的关键,是当事务回滚时能够撤销所有已经执行成功的SQL语句,InnoDB实现回滚,靠的是undo log,当事务对数据库进行修改时,InnoDB会生成当前数据对应的副本信息;如果事务执行失败或者调用了rollback,导致事务需要回滚,就利用undo log中的信息将数据回滚到修改之前的样子
- undo log属于逻辑日志,它记录的是SQL执行相关的信息,当发生回滚时,InndDB会根据undo log的内容做与之前相反的工作,对于每个insert,回滚时会执行delete,对于每个update,回滚时会执行一个相反的update,将数据改回去。
- 以update操作为例,当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行),修改了哪些列、这些列在修改前后的值的信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
redo log存在的背景
redo log与undo log都属于InnoDB的事务日志,下面聊一下redo log存在的背景
InnoDB作为MySQL的存储引擎,数据时放在磁盘的,但是如果每次读写数据都需要磁盘IO,效率会很低,为此,InnoDB提供了缓存(Buffer Pool),BP中包含了部分数据页的映射,作为访问数据库的缓冲;当从数据库读取数据时,会首先写入BP,BP中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)
BP的使用大大提高了读写数据的效率,但是也带来了新的问题,如果MySQL宕机,而此时BP中修改的数据还没有刷新的磁盘,就会导致数据的丢失,事务的持久性无法保证。
✳持久性实现原理 redo log
redo log就被引入来解决这个问题(宕机导致BP中的数据没有刷新磁盘,造成数据丢失),当数据被修改时,除了修改BP中的数据,还会在redo log中记录这次操作,当事务提交时,会调用fsync接口对 redo log进行刷盘,如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复,redo log采用的是WAL(Write-ahead logging,预写式日志), 所有修改先写入日志,在更新到BP,保证了数据不会因为MySQL宕机而丢失,从而满足了持久性的要求。
既然redo log也需要在事务提交是将日志写入磁盘,为什么它比直接将BP中修改的数据写入磁盘(即刷脏)要快呢?
- 刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO
- 刷脏是以数据页(Page)为单位的,MySQL默认页的大小是16KB,一个Page上一个小修改都要整页写入,而redo log中只包含真正需要写入的部分,无效IO大大的减少。
✳隔离性实现原理
隔离性定义
隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性对应了事务的隔离级别中的Serializable(可串行化),但实际应用中处于性能考虑很少会使用此级别
隔离性追求的是并发情况下事务之间互不干扰,简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊情况会特殊说明),那么隔离性的探讨,主要可以分为两个方面。
- (一个事务)的写操作对(另一个事务)写操作的影响:锁机制保证隔离性
- (一个事务)的写操作对(另一个事务)读操作的影响: MVCC保证隔离性
锁机制原理的简单阐述
锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。(共享锁和独占锁)
后续展望
由于锁机制和MVCC也属于MySQL的核心难点,我们这里就不在过多阐述,详情见后续文章。
✳一致性实现原理
一致性定义
一致性是指事务执行的结果必须是使数据库从一个一致性状态变到另一个一致状态,即数据库的完整性没有被破坏,事务执行的前后都是合法的状态。
实现
可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面的保障。
实现一致性的措施包括:
- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致
总结
下面总结一下ACID特性及其实现原理:
- 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
- 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
- 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR(可重复读),RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
- 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障