MySQL事务
目录
一、事务的四大特性
二、 事务的类型
三、MySQL的ACID事务特性的实现
四、MySQL的事务隔离级别
五、MySQL事务隔离级别产生脏读、不可重复读、幻读的原因
六、ACID特性的实现原理
1、原子性:
2、持久性
3、隔离性
4、一致性
5、总结ACID特性及其实现原理
一、事务的四大特性
事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。在事务中的操作,要么都执行修改,要么都不执行,这就是事务的目的,也是事务模型区别于文件系统的重要特征之一。
事务需遵循ACID四个特性:
- A(atomicity),原子性。单个事务,为一个不可分割的最小工作单元,整个事务中的所有操作要么全部commit成功,要么全部失败rollback,对于一个事务来说,不可能只执行其中的一部分SQL操作,这就是事务的原子性。
- C(consistency),一致性。一致性指事务将数据库从一种状态转变为另一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- I(isolation),隔离性。多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- D(durability) ,持久性。一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
二、 事务的类型
- 扁平事务:是事务类型中最简单的一种,而在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束。处于之间的操作是原子的,要么都执行,要么都回滚。
- 带有保存点的扁平事务:除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为可能某些事务在执行过程中出现的错误并不会对所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(savepoint)用来通知系统应该记住事务当前的状态,以便以后发生错误时,事务能回到该状态。
- 链事务:链接事务是指一项事务在提交时自动将上下文传递给下一项事务,即一项事务的提交和下一项事务的开始是原子性的,下一项事务可以看到上一项事务的结果,就像在一项事务中进行一样。链式事务可以看作是保存点模式的一个变种,不同的是,带保存点的扁平事务可以回到任何正确的保存点,而链式事务的回滚仅限于当前事务。
- 嵌套事务:由一个顶层事务控制不同层次的事务。嵌套在顶层事务下的事务叫做子事务,它控制着每个局部的变化。子事务提交后,不会真正提交,而是等到父事务提交后才真正提交。父事务回滚后,所有子事务都会回滚。(MySQL不支持)
- 分布式事务:通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。对于分布式事务,同样需要满足ACID特性,要么都发生,要么都失效。
三、MySQL的ACID事务特性的实现
1、原子性实现原理:
MySQL的原子性是通过Undo Log实现的。Undo Log用于实现事务的原子性,理解为逻辑日志,记录相反操作。Undo Log的作用有两个:回滚和多版本控制。回滚是指在事务执行过程中发生错误或者用户手动回滚时,可以通过Undo Log将数据恢复到事务开始之前的状态。多版本控制是指在并发环境下,为了保证读写数据的一致性,需要对数据进行多版本控制,即在每次修改数据时,都会生成一个新版本。
2、持久性实现原理:
持久性是指事务提交后,对数据库的修改将会永久保存在数据库中。在MySQL中,持久性是通过Redo Log实现的。Redo Log用于保证事务的持久性,记录事务对数据库所做的修改操作。当事务提交时,将Redo Log中的记录写入磁盘,从而保证了数据的持久性。
3、隔离性实现原理:
在MySQL中,隔离性是通过锁机制实现的。当多个事务同时对同一数据进行操作时,为了保证数据的一致性,MySQL会对这些事务进行隔离,使它们互不干扰。MySQL提供了四种隔离级别:读未提交、读已提交、可重复读和串行化。不同的隔离级别会使用不同的锁机制来保证事务的隔离性。
4、一致性实现原理:
在MySQL中,一致性是通过回滚日志实现的。当事务回滚时,MySQL会将事务对数据库所做的修改操作记录在回滚日志中。回滚日志用于记录事务对数据库所做的修改操作,当事务回滚时,将回滚日志中的记录恢复到数据库中,从而保证了数据的一致性。
四、MySQL的事务隔离级别
-
读未提交(READ UNCOMMITTED):它允许一个事务读取另一个事务还未提交的数据。这种隔离级别会导致脏读、不可重复读和幻读等问题。
-
读已提交 (READ COMMITTED):指一个事务只能读取另一个事务已经提交的数据。这种隔离级别可以避免脏读问题,但是可能会出现不可重复读和幻读问题。
-
可重复读 (REPEATABLE READ):在同一个事务中,多次读取同一数据时,其结果是一致的。这种隔离级别可以避免脏读和不可重复读问题,但是可能会出现幻读问题。
-
可串行化 (SERIALIZABLE):是最高的隔离级别,它通过强制事务串行执行来避免脏读、不可重复读和幻读等问题。这种隔离级别会对性能产生较大的影响。
并发情况下,读操作可能存在的三类问题:
- 脏读:是指一个事务读取到另一个事务还未提交的数据。
- 不可重复读:是指在同一个事务中,两次查询的数据不一致。
- 幻读:是指在同一个事务中,两次查询的数据行数不一致。
五、MySQL事务隔离级别产生脏读、不可重复读、幻读的原因
1、读未提交:
- 脏读:指一个事务可以读取另一个未提交的事务的数据,这样可能会导致脏读,即读到了未提交的数据。
- 不可重复读:是指在同一个事务中,多次读取同一数据,但是由于其他事务对该数据进行了修改或删除,导致前后两次读取的结果不同。
- 幻读:是指在同一个事务中,多次执行同一查询,但是由于其他事务对该表进行了插入或删除操作,导致前后两次查询的结果不同。
2、读已提交:
- 不可重复读、幻读:是指一个事务只能读取已经提交的数据,所以可以避免脏读。但是不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据,所以不能避免不可重复读和幻读现象。
3、可重复读:
- 幻读:在可重复读的隔离级别下,事务1读取了一些数据,事务2插入了一些新数据,然后事务1再次读取相同的数据,此时会发现有新的数据行,这就是幻读。
六、ACID特性的实现原理
1、原子性:
定义:
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
实现原理(undo log):
在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。
下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,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):
redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。
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被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
- 刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
- 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
3、隔离性
1、(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性。
隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁。获得锁之后,事务便可以修改数据。该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差。行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源,因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
2、(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性。
InnoDB默认的隔离级别是RR(REPEATABLE READ),RR解决脏读、不可重复读、幻读等问题,使用的是MVCC。MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。它最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:
-
隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
-
基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
-
ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。
4、一致性
可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。
实现一致性的措施包括:
- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致
5、总结ACID特性及其实现原理
- 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log。
- 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log。
- 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)。
- 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障。