MySQL高级进阶(八)、事务
InnoDB
存储引擎中的事务完全符合ACID特性:
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
原子性(Atomicity
)指整个数据库事务是不可分割的工作单位,事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性(Consistency
)指事务将数据库从一种状态转变为下一种一致性状态。在事务开始之前和事务结束之后,数据库的完整性没有被破坏。
隔离性(Isolation
)要求每个读写事务的对象对其他事务的操作对象能互相分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。
持久性(Durability
)事务一旦提交,其结果是永久性的,即便系统故障也不会丢失。
事务的分类:
- 扁平事务
- 带有保存点的扁平事务
- 链事务
- 嵌套事务
- 分布式事务
在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK
开始,由COMMIT WORK
或ROLLBACK WORK
结束,其间的操作都是原子操作,要么多执行,要么都回滚。因此扁平事务是应用程序成为原子操作的基本组成模块。
带保存点的扁平事务,除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点时的状态。保存点用SAVE WORK
函数来创建。
链事务可视为保存点模式的一种变种。带有保存点的扁平事务,当系统发生宕机时,所有的保存点都消失,因为保存点是易失的而非持久的。这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。链事务的设计思想是,在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。提交事务和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。
链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近一个保存点。链事务在执行COMMIT
后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
嵌套事务是一个层次结构框架。有一个顶层事务控制着各个层次的事务。顶层事务之下嵌套的事务称为子事务,其控制着每一个局部的变换。
- 嵌套事务是由若干个事务组成的一棵树,子树既可以是嵌套事务,也可以是扁平事务。
- 处在叶子节点的事务是扁平事务。但是每个子事务从根节点到叶节点的距离可以是不同的。
- 处于根节点的事务称为顶层事务,其他事务称为子事务。事务的前驱称为父事务,事务的下一层称为儿子事务。
- 子事务既可以提交也可以回滚。但它的提交操作并不马上生效,除非其父事务已经提交。任何子事务都在顶层事务提交后才真正生效。
- 树中的任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留
ACI
特性,不具备D
特性。
分布式事务通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
对于InnoDB
存储引擎来说,其支持扁平事务、带有保存点的扁平事务、链事务、分布式事务。对于嵌套事务,其并不原生支持。
原子性、一致性、持久性通过数据库的redo log
和undo log
来完成。redo log
称为重做日志,用来保证事务的原子性和持久性。undo log
用来保证事务的一致性。
redo log
恢复提交事务修改的页操作,而undo log
回滚行记录到某个特定版本。redo通常是物理日志,记录的是页的物理修改操作。undo
是逻辑日志,根据每行记录进行记录。
redo
重做日志用来实现事务的持久性,即事务的ACID
中的D
。其由两部分组成:一是内存中的重做日志缓存(redo log buffer
),其是易失性的;二是重做日志文件(redo log file
),其实持久的。
InnoDB
存储引擎是事务的存储引擎,其通过Force Log at Commit
机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit
操作完成才算完成。这里的日志是指重做日志,在InnoDB
存储引擎中由两部分组成,即redo log
和undo log
。redo log
用来保证事务的持久性,undo log
用来帮助事务回滚及MVCC
的功能。redo log
基本上都是顺序写的,在数据库运行时不需要redo log
的文件进行读取操作。而undo log
是需要进行随机读写的。
为了确保每次日志都 写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB
存储引擎都需要调用一次fsync
操作。由于重做日志文件打开并没有使用O_DIRECT
选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync
操作。由于fsync
的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。
在MySQL
数据库中还有一种二级制日志(binlog
),其用来进行POINT-IN-TIME(PIT)
的恢复及主从复制环境的建立。从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。首先,重组日志是在InnoDB
存储引擎层产生的,并且二进制日志不仅仅针对InnoDB
存储引擎,MySQL
数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。其次,两种日志记录的内容形式不同。MySQL
数据库上层的二进制日志是一种逻辑日志,其记录的是对应的SQL
语句。而InnoDB
存储引擎层面的重做日志是物理格式日志,其记录的是对每个页的修改。此外,两种日志记录写入磁盘的时间点不同。二级制日志只在事务提交完成后进行一次写入。而InnoDB
存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。
在InnoDB
存储引擎中,重做日志都是以512
字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block
)的方式进行保存的,称之为日志块(redo log block
),美块的大小为512
字节。
若一个页中产生的重做日志数量大于512
字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区的大小一样,都是512
字节,因此重做日志的写入可以保证原子性,不需要doublewrite
技术。
重做日志块除了日志本身之外,还由日志块头(log block header
)及日志块尾(log block tailer
)两部分组成。重做日志头一共占用12
字节,重做日志尾占用8
字节。故每个重做日志块实际可以存储的大小为492
字节。
log group
为重做日志组,其中有多个重做日志文件。虽然源码中已支持log group
的镜像功能,但是在ha_innobase.cc
文件中禁止了该功能,因此InnoDB
存储引擎实际只有一个log group
。
log group
是一个逻辑上的概念,并没有一个实际存储的物理文件来表示log group
信息。log group
由多个重做日志文件组成,每个log group
中的日志文件大小是相同的,且在InnoDB1.2
版本之前,重做日志文件的总大小要小于4GB
。从InnoDB1.2
开始重做日志总大小的限制提高为512GB
。
重做日志文件中存储的就是之前在log buffer
中保存的log block
,因此其也是根据块的方式进行物理存储的管理,每个块的大小与log block
一样,同样为512
字节。在InnoDB
存储引擎运行过程中,log buffer
根据一定的规则将内存中的log block
刷新到磁盘。
由于InnoDB
存储引擎的存储管理是基于页的,故其重做日志格式也是基于页的。
LSN
是Log Sequence Number
的缩写,其代表的是日志序列号。在InnoDB
存储引擎中,LSN
占用8
字节,并且单调递增。LSN
表示的含义有:
- 重做日志写入的总量
checkpoint
的位置- 页的版本
假如页P1
的LSN
为10000
,而数据库启动时,InnoDB
检测到写入重做日志中的LSN
为13000
,并且事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到P1
页中。
InnoDB
存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志,要快很多。与此同时,InnoDB
存储引擎自身页对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步提高数据库恢复的速度。
由于checkpoint
表示已经刷新到磁盘页上的LSN
,因此在恢复过程中仅需恢复checkpoint
开始的日志部分。
undo
重做日志记录了事务的行为,可以很好地通过其对页进行重做操作。但是事务有时还需要进行回滚操作,这时就需要undo
。因此在对数据库进行修改时,InnoDB
存储引擎不但会产生redo
,还会产生一定量的undo
。如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK
语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。
redo
存放在重做日志文件中,与redo
不同,undo
存放在数据库内部的一个特殊段(segment
)中,这个段称为undo
段。undo
段位于共享表空间内。
当用户执行ROLLBACK
时,会将插入的事务进行回滚,但是表空间的大小并不会因此而收缩。因此,当InnoDB
存储引擎回滚时,它实际上做的是与先前相反的工作。对于每个INSERT
,InnoDB
存储引擎会完成一个DELETE
,对于每个DELETE
,InnoDB
存储引擎会执行一个INSERT
,对于每个UPDATE
,InnoDB
存储引擎会执行一个相反的UPDATE
,将修改前的行放回去。
除了回滚操作,undo
的另一个作用是MVCC
,即在InnoDB
存储引擎中MVCC
的实现是通过undo
完成的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo
读取之前的行版本信息,以此实现非锁定读取。
undo log
会产生redo log
,也就是undo log
的产生会伴随着redo log
的产生,这是因为undo log
也需要持久性的保护。
InnoDB
存储引擎对undo
的管理同样采用段的方式。首先InnoDB
存储引擎有rollback segment
,每个回滚段记录了1024
个undo log segment
,而在每个undo log segment
段中进行undo
页的申请。共享表空间偏移量为5
的页(0,5)
记录了所有rollback segment header
所在的页,这个页的类型为FIL_PAGE_TYPE_SYS
。
事务在undo log segment
分配页并写入undo log
的这个过程同样需要写入重做日志。当事务提交时,InnoDB
存储引擎会做以下两件事情:
- 将
undo log
放入列表中,以供之后的purge
操作 - 判断
undo log
所在的页是否可以重用,若可以分配给下个事务使用
事务提交后并不能马上删除undo log
及undo log
所在的页。因为可能还有其他事务需要通过undo log
来得到行记录之前的版本。故事务提交时将undo log
放入一个链表中,是否可以最终删除undo log
及undo log
所在的页由purge
线程判断。
若为每一个事务分配一个单独的undo
页会非常浪费存储空间,特别是对于OLTP
类型的应用。在InnoDB
存储引擎的设计中对undo
页可以进行重用。当事务提交时,首先将undo log
放入链表中,然后判断undo
页的使用空间是否小于3/4
,若是则表示undo
页可以被重用,之后新的undo log
记录在当前undo log
的后面。由于存放undo log
的列表是已记录进行组织的,而undo
页可能存放着不同事务的undo log
,因此purge
操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程。
在InnoDB
存储引擎中,undo log
分为:
insert undo log
update undo log
insert undo log
是指在insert
操作中产生的undo log
。因为insert
操作的记录,只对事务本身可见,对其他事务不可见(这时事务的隔离性要求),故该undo log
可以在事务提交后直接删除。不需要进行purge
操作。
update undo log
记录的是对Delete
和update
操作产生的undo log
。该undo log
可能需要提供MVCC
机制,因此不能再事务提交时就进行删除。提交时放入undo log
链表,等待purge
线程进行最后的删除。update undo log
相对于之前介绍的insert undo log
,记录的内容更多,所需占用的空间也更大。
purge
Delete
和update
操作可能并不直接删除原有的数据。purge
用于最终完成Delete
和update
操作。因为InnoDB
存储引擎支持MVCC
,所以记录不能在事务提交时立即进行处理,这时其他事务可能正在引用这行,故InnoDB
存储引擎需要保存记录之前的版本。而是否可以删除这条记录通过purge
来进行判断。若该行记录已不被任何其他事务引用,那么久可以进行真正的Delete
操作。可见,purge
操作时清理之前的Delete
和update
操作,将上述操作最终完成。而实际执行的操作为Delete
操作,清理之前行记录的版本。
为了节省存储空间,InnoDB
存储引擎的undo log
设计是这样的:一个页上允许多个事务的undo log
存在。虽然这并不代表事务在全局过程中提交的顺序,但是后面的事务产生的undo log
总在后面。此外,InnoDB
存储引擎还有一个history
列表,它根据事务提交的顺序,将undo log
进行链接。history list
表示按照事务提交的顺序将undo log
进行组织。在InnoDB
存储引擎的设计中,先提交的事务总在尾端。undo page
存放了undo log
,由于可以重用,因此一个undo page
中可能存放了多个不同事务的undo log
。
InnoDB
存储引擎这种先从 history list
中找undo log
,然后再从undo page
中找undo log
的设计模式是为了避免大量的随机读取操作,从而提高效率。
若事务为非只读事务,则每次事务提交时需要进行一次fsync
操作,以此保证重做日志已经写入磁盘。当数据库发生宕机时,可以通过重做日志进行恢复。为了提高磁盘fsync的效率,当前数据库都提供了group commit
的功能,即一次fsync
可以刷新确保多个事务日志被写入文件。对于InnoDB
存储引擎来说,事务提交时会进行两个阶段的操作:
- 修改内存中事务对应的信息,并且将日志写入重做日志缓冲
- 调用
fsync
将确保日志都从重做日志缓冲写入磁盘
可以将多个事务的重做日志通过一次fsync
刷新到磁盘,这样大大地减少了磁盘的压力,从而提高了数据库的整体性能。对于写入或更新较为频繁的操作,group commit
的效果尤为明显。
分布式事务
在使用分布式事务时,InnoDB
存储引擎的事务隔离级别必须设置为SERIALIZABLE
。XA事务允许不同数据库之间的分布式事务,如一台服务器是MySQL
数据库,另一台是Oracle
数据库的,又可能还有一台服务器是SQL Server
数据库,只要参与在全局事务中的每个节点都支持XA
事务。
XA
事务由一个或多个资源管理器(Resource Managers
)、一个事务管理器(Transaction Manager
)以及一个应用程序(Application Program
)组成。
- 资源管理器:提供访问事务资源的方法。通常一个数据库就是一个资源管理器。
- 事务管理器:协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器通信。
- 应用程序:定义事务的边界,指定全局事务中的操作。
分布式事务使用两阶段提交(two-phase commit
)的方式。在第一阶段,所有参与全局事务的节点都开始准备(prepare
),告诉事务管理器他们准备好提交了。在第二阶段,事务管理器告诉资源管理器执行ROLLBACK
还是COMMIT
。如果任何一个节点显示不能提交,则所有 的节点都被告知需要回滚。可见与本地事务不同的是,分布式事务需要多一次的PREPARE
操作,待收到所有节点的同意信息后,再进行COMMIT
或是ROLLBACK
操作。
最为常见的内部XA事务存在于binlog
于InnoDB
存储引擎直接。由于复制的需要,因此目前绝大多数的数据库都开启了binlog
功能。在事务提交时,先写二进制日志,再写InnoDB
存储引擎的重做日志。