MySQL技术内幕-InnoDB存储引擎-第七章、事务
文章目录
事务
事务:事务会把数据库从一种一致状态转化为另外一种一致状态,在数据库提交工作时,可以确保要么所有修改已经保存了,要么所有修改都不保存。
事务的四个特性(ACID):
- 原子性
- 一致性
- 隔离性
- 持久性
第六章介绍的锁谈论了事务如何实现隔离性的,本章主要关注原子性这个概念。
一、认识事务(要么所有修改,要么都不保存)
1、概述
事务可以由一条简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作要,饿都成功,要么都不做。
Oracle数据库默认隔离级别为READ COMMITTED,不满足隔离性的要求,而InnoDB默认的隔离级别是READ REPEATABLE,完全遵循和满足事务的ACID特性。
原子性(事务中的操作要么全部成功,要么全部失败)
取钱例子:
- 登陆ATM取钱,验证密码
- 从远程银行数据库中,取得账户信息
- 用户在ATM上输入想提取的金额
- 从远程银行的数据库中更新账户信息
- ATM出款
- 用户取钱
整个取钱过程应该被看作原子操作,即要么都做,要么都不做。
原子性指整个数据库事务是一个不可分割的工作单元,只有使事务中所有的数据库操作都执行成功,才算整个事务成功。
一致性(事务将数据库从一种状态转变为下一种一致的状态)
一致性指的是事务将数据库从一种状态转变为下一种一致的状态
如果事务中的某个动作失败了,系统可以自动撤销事务,返回初始化的状态。
隔离性(要求每个读写事务的对象对其他事务的操作能相互隔离)
隔离性要求每个读写事务的对象对其他事务的操作对象能相互隔离,即该事务的提交对其他事务都是不可见的,通常使用锁来实现。
持久性(事务一旦提交,结果就是永久性的)
事务一旦提交,结果就是永久性的。即使发生宕机等故障。数据库也能将数据恢复好。
2、分类
从事务理论的角度来说,可以把事务分为下面几种类型:
- 扁平事务
- 带有保存点的扁平事务
- 链事务
- 嵌套事务
- 分布式事务
扁平事务(最简单、使用最频繁)
在扁平事务中,所有操作都处于同一层次。
扁平事务由begin work开始,由commit work或者rollback work结束,其间的操作是原子的。要么都执行,要么都回滚。
扁平事务的主要限制是不能提交或者回滚事务的某一部分。或分几个步骤提交。
下面的旅行度假例子使用扁平式事务就不可以完成:
- begin work
- 预定杭州到上海的高铁
- 上海浦东国际机场坐飞机,预定去米兰的航班
- 在米兰转火车前往佛罗伦萨,预定去佛罗伦萨的火车。
如果到达米兰太晚了,没有当天火车,这时希望在米兰住一晚,第二天去佛罗伦萨,如果事务是扁平事务,则要回滚之前的操作,这个代价有点大。
因此就出现了带有保存点的扁平事务
带有保存点的扁平事务:除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为,某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效。
保存点:用来通知系统应该记住事务当前的状态,以便当之后发生错误的时候,事务能回到保存点当时的状态。
可以根据逻辑决定回到最近的一个保存点还是更早的保存点。
保存点在事务内部是递增的。rollback是不影响保存点计数的。如下图
链事务:
- 带有保存点的扁平事务,当发生系统崩溃的时候,所有保存点都将消息,因为其保存点是易失的。这就意味着当进行恢复的时候,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。
- 链事务的思想是:在提交事务的时候,释放不需要的数据对象,将必要的处理上下文隐式传给下一个要开始的事务。注意:提交事务操作和开始下一个事务操作将合并为一个原子操作。
嵌套事务:
嵌套事务是一个层次结构框架,由一个顶层事务控制各个层次的事务。顶层事务之下嵌套的事务被称为子事务。
嵌套事务的定义:
- 嵌套事务是由若干事务组成的一棵树,子树既可以是嵌套事务,也可以是扁平事务。
- 处在叶节点的事务是扁平事务,但是每个事务从根到叶节点的距离可以是不同的。
- 位于根节点的事务称为顶层事务,其他事务为子事务,事务的前驱称为父事务,事务的下一层称为儿子事务
- 子事务既可以提交也可以回滚,但是他的操作不是立马生效,除非其父事务提交。
- 树中的任何一个事务的回滚都会引起它的所有子事务一同回滚,故子事务保留ACI特性,不具有D的特性。
实际的工作是交给叶子节点来完成的,即只有叶子节点的事务才能访问数据库、发送消息、获取其他类型的资源。即高层的事务仅负责逻辑控制。
分布式事务:
通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
假设一个用户在ATM机进行银行的转账操作,例如持卡人从招商银行的储蓄卡转账10000到工商银行的储蓄卡,在这种情况下,可以将ATM视为节点A,招商银行的后台数据库视为节点B,工商银行的后台数据库视为C,这个转账的操作可分解为以下的步骤:
- 节点A发出转账命令
- 节点B执行储蓄卡中的余额减去10000
- 节点C执行储蓄卡中的余额加上10000
- 节点A通知用户操作完成或者节点A通知用户操作失败。
这里需要使用分布式事务,因为节点A不能通过调用一个数据库就完成,其中需要访问网络中的两个节点的数据库。而在每个节点的数据库执行的事务操作又都是扁平的,对于分布式事务,同样需要满足ACID特性,要么都发生,要么都失败。
二、事务的实现(原子性、一致性、持久性通过数据库的redo log、undo log来完成)
事务的隔离性由锁来实现。
原子性、一致性、持久性通过数据库的redo log、undo log来完成,redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
有的DBA认为undo是redo的逆过程,其实不然,redo和undo的作用都可以视为一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作,undo是逻辑日志,根据每行记录进行记录。
1、redo
基本概念
重做日志用来实现事务的持久性,即事务ACID中的D。
它由两部分组成:
- 内存中的重做日志缓冲,易失的
- 重做日志文件,持久的
InnoDB通过Force Log at Commit机制实现事务的持久性,即当事务提交的时候,必须先将该事务的日志写入到重做日志文件进行持久化。待事务的COMMIT操作完成才算完成
redo log和undo log,redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能,redo log基本上是顺序写的,在数据库允许的时候不需要对redo log的文件进行读取操作,而undo log需要进行随机读写的。
为了保证每次日志肉写入重做日志文件,在每次将重做日志缓冲写入重做日志文件之后,InnoDB存储引擎都需要调用一次fsync操作,由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓冲,为了确保日志文件写入磁盘,必须进行一次fsync操作,由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。
是否每次事务提交是都进行一次fsync操作可以通过参数innodb_flush_log_at_trx_commit控制:
- 参数innodb_flush_log_at_trx_commit用老枝重做日志刷新到磁盘策略。
- 默认为1,表示事务提交的时候必须调用一次fsync。
- 还可以设置为0和2。
- 0表示事务提交的时候不进行写入重做日志操作,这个操作仅在master thread中完成,master thread会1s进行一次重做日志的fsync操作。
- 2表示事务提交的时候将重做日志写入重做日志文件。但是仅仅写入文件系统的缓存中,不进行fsync操作,所以数据库宕机,数据不会丢失,因为在文件系统缓存中,如果操作系统宕机,数据会丢失。
执行CALL p_load(500000)会向表中插入50万行的记录,并执行50w次的fsync操作,下面是插入50w条记录所需的时间即差不多两分钟:
将参数innodb_flush_log_at_trx_commit设置为0的情况:
可以看到设置成0之后,插入50w行记录的时间缩短为13.9s。差不多是之前的12%。说明后者大大减少了fsync的次数,提高了数据库执行的性能。
binlog(二进制日志)
用来进行POINT-IN-TIME的恢复以及主从复制环境的建立。
从表面看其和重做日志非常相似,都是记录对于数据库操作的日志, 然而,从本质上看,两者有着很大的不同。
- 重做日志是InnoDB存储引擎层产生的
- 二进制日志是MySQL上层产生,所以任何存储引擎对于数据库的更改都会产生二进制日志
- 二进制日志是一种逻辑体制,记录的是SQL语句,重做日志是物理格式日志,记录的是对于每个页的修改。
-
二进制日志只在事务提交完成后进行一次写入,而在InnoDB存储引擎的重做日志在事务进行中不断的被写入,这表现为日志并不是随事务提交的顺序进行写入的。
2、log block
在InnoDB中,重做日志都是以512字节进行存储的,这意味着重做日志缓存、重做日志文件都是以块的方式进行保存的,称之为重做日志块。
如果一个页产生的重做日志数量大于512字节,那么需要分割为重做日志块进行存储。此外,由于重做日志块的大小和扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术。
重做日志除了日志本身之外,还由日志块头(12字节)及日志块尾(8字节)两部分组成。所以每个重做日志块实际可以存储的大小为492字节。
3、log group(由多个重做日志文件组成)
log group由多个重做日志文件组成,每个log group中的日志文件大小是相同的。InnoDB1.2之前重做日志文件大小要小于4GB,从InnoDB1.2开始,重做日志文件总大小的限制提高为512GB。
重做日志文件中存储的就是之前logBuffer中的log block。
在InnoDB存储引擎执行的过程中,log buffer根据一定的规则将内存中的log block刷新到磁盘。这个具体规则是:
- 事务提交的时候
- 当log buffer中有一般的内存空间已经被使用时
- log checkpoint时
略
4、重做日志格式
- redo_log_type:重做日志的类型
- space:表空间ID
- page_no:页的偏移量
redo log body的部分,根据重做日志类型的不同,会有不同的存储内容。例如:页上记录的插入和删除,
在InnoDB1.2的时候,一共有51种重做日志类型
5、LSN(Log Sequence Number日志***)
LSN在redo log中有,在页中也有。redo log记录的是每个页的日志,所以LSN应该就是对应于1个页的。
占用8字节,单调递增。LSN表示的含义有:
- 重做日志写入的总量
- checkpoint的位置
- 页的版本
LSN表示事务写入重做日志的字节的总量,例如当前重做日志LSN为1000,有1个事务写了100字节的重做日志,那么LSN就变成了1100.
LSN不仅记录在重做日志中,还存在于每个页中。在每个页的头部有一个值FILE_PAGE_LSN,记录了该页的LSN。表示最后刷新时候的LSN的大小。因为重做日志记录的是每个页的日志,因此页中的LSN用来判断是否需要进行恢复操作。例如,页P1的LSN为10000,数据库启动的时候,InnoDB监测到写入重做日志的LSN为13000,并且事务已经提交,那么数据库需要进行恢复操作。将重做日志应用到P1中。