InnoDB事务
1.事务概述
事务是数据库区别于文件系统的重要特征之一. 事务会把数据库从一种一致状态转换为另一种一致状态. 数据库提交工作时, 可以确保要么所有修改都已经保存了, 要么所有修改都不保存
InnoDB存储引擎中的事务完全符合ACID的特性, ACID是以下四个词的缩写 :
- 原子性 : 原子性是指整个数据库事务是不可分割的工作单位. 只有事务中所有的数据库操作都执行成功,才算整个事务成功. 事务中任何一个SQL语句执行失败, 已经执行成功的SQL语句也必须撤销 , 数据库状态应该退回到执行事务前的状态
- 一致性 : 一致性指事务将数据库从一种状态转变为下一种一致的状态. 在事务开始之前和事务结束之后, 数据库的完整性约束没有被破坏.
- 隔离性 : 事务的隔离性要求每个读写事务的对象对其他事务的操作能相互分离, 即该事务提交前对其他事务都不可见
- 持久性 : 事务一旦提交, 其结果就是永久性的
2.事务分类
从事务的理论角度来说, 可以把事务分为一下几种类型
- 扁平事务
- 带有保存点的扁平事务
- 链事务
- 嵌套事务
- 分布式事务
扁平事务 : 是事务类型中最简单的一种, 但在实际生产环境中可能是使用最频繁地事务. 在扁平事务中, 所有操作都处于同一层次, 其间的操作是原子的, 要么都执行, 要么都回滚. 扁平式事务的主要限制是不能提交或回滚事务的一部分, 或分几个步骤提交. 因此出现了带有保存点的扁平事务
带有保存点的扁平事务 : 除了支持扁平事务支持的操作外, 允许事务执行过程中回滚到同一事务中较早的一个状态. 因为某些事务在执行过程中出现的错误并不会导致所有的操作都无效, 放弃整个事务不合乎要求, 开销也太大. 保存点用来通知系统记住事务当前的状态, 以便发生错误后, 事务能回到保存点当前的状态****
链事务 : 可视为保存点模式的一种变种. 带有保存点的扁平事务, 当发生系统崩溃时, 所有的保存点都将小时, 因为其保存点是易失的, 而非持久的, 这意味着进行恢复时, 食物需要从开始处重新执行, 而不能从最近的一个保存点继续执行. 链事务的思想是 : 在提交一个事务时, 释放不需要的数据对象, 将必要的处理上下文隐式地传给下一个要开始的事务. 注意, 提交事务操作和开始下一个事务操作将合并为一个原子操作. 链事务与带有保存点的事务不同的是, 带有保存点的扁平事务能回滚到任意正确的保存点, 而链事务中的回滚仅限于当前事务. 另外, 链事务在执行了COMMIT后即释放了当前事务所持有的锁, 而带有保存点的扁平事务不影响迄今为止所持有的锁
嵌套事务 : 是一个层次结构框架, 由一个顶层事务控制着各个层次的事务. 顶层事务之下嵌套的事务被称为子事务. 实际的工作是交由叶子节点完成的, 即只有叶子节点的事务才能访问数据库, 发送消息, 获取其他类型的资源. 而高层的事务仅负责逻辑控制, 决定何时调用相关的子事务.
分布式事务 : 通常是一个在分布式环境下运行的扁平事务, 因此需要根据数据在所在位置访问网络中的不同节点.
3.事务的实现
事务的隔离性由加锁实现. 原子性, 一致性, 持久性通过数据库的redo log和undo log来完成. redo log称为重做日志, 用来保证事务的原子性和持久性. undo log用来保证事务的一致性
redo和undo 的作用都可以视为是一种恢复操作, redo恢复提交事务修改的页操作, undo回滚行记录到某个特定版本. 因此两者记录内容不同, redo通常是物理日志, 记录的是页和物理修改操作. undo是逻辑日志, 根据每行记录进行记录
1>redo
基本概念 : 重做日志用来实现事务的持久性, 即事务ACID中的D. 其由两部分组成, 一个是内存中的重做日志缓存, 其是易失的; 二是重做日志文件, 其是持久的
InnoDB是事务的存储引擎, 其通过 Force Log at Commit机制实现事务的持久性, 即当事务提交时, 必须先将该事物的所有日志写入到重做日志文件进行持久化, 待事务的COMMIT操作完成才算完成.
为了保证每次日志都写入重做日志文件, 在每次将重做日志缓冲写入日志文件后, InnoDB都需要调用一次fsync操作. 重做日志缓冲先写入文件系统缓冲, 为了 确保重做日志写入磁盘, 必须进行一次fsync操作. 由于fsync的效率取决于磁盘的性能, 因此磁盘的性能决定了事务提交的性能.
在MySQL数据库中还有一种二进制文件(binlog), 其用来进行POINT-IN-TIME的恢复和主从复环境的建立. 从表面看其和重做日志很相似, 都是记录了对数据库操作的日志, 然而从本质上看两者有非常大的不同
- 首先, 重做日志实在InnoDB存储引擎层产生, 而二进制日志是在MySQL数据库的上层产生的, 不仅仅针对InnoDB, MySQL任何存储引擎对于数据库的更改都会产生二进制文件
- 其次, 两种日志记录的内容形式不一样. MySQL数据库上层的二进制日志是一种逻辑日志, 其记录的是对应的SQL语句. 而InnoDB存储引擎层面的重做日志是物理格式的日志, 其记录的是对于每个页的修改
- 此外 , 两种日志记录写入磁盘的时间点不同, 二进制日志只在事务提交完成后进行一次写入. 而InnoDB存储引擎的重做日志在事务进行中不断地被写入, 这表现为日志并不是随事务提交的顺序进行写入的.
log block :
在InnoDB中, 重做日志都是以512字节进行存储的, 这意味着重做日志缓冲, 重做日志都是以块的方式进行保存的. 称之为重做日志块(redo log block)
若一个页中产生的重做日志数量大于512字节, 那么需要分割为多个重做日志块进行存储. 此外, 由于重做日志块的大小和磁盘山区大小一样, 都是512字节, 因此重做日志的写入可以保证原子性, 不需要doublewrite技术.
重做日志缓存的每个日志块可以分为三部分 : 日志块头, 日志内容, 日志块尾
log group :
log group为重做日志组, 其中有多个重做日志文件. log group由多个重做日志文件组成, 每个log group中的日志文件大小是相同的. 重做日志文件中存储的就是之前在log buffer中保存的log block, 因此其也是根据块的方式进行物理存储的管理, 每个块的大小与log back一样, 同样为512字节. log buffer根据一定的规则将内存中的log block刷新到磁盘 , 规则具体是 :
- 事务 提交时
- 当log buffer中有一般的内存空间已经被使用时
- log checkpoint时
对于log block的写入追加在redo log file的最后部分, 当一个redo log file被写满时, 会接着写入下一个redo log file
恢复 :
InnoDB存储引擎在启动时不管上一次数据库运行是否正常关闭, 都会尝试进行恢复操作. 因为重做日志记录的是物理日志, 因此恢复的速度比逻辑日志, 如二进制日志, 要快很多. 我理解的物理日志就是说 : 直接记录事务对于B+树结构和节点值的修改. 而逻辑日志是记录事务的每一句SQL语句, 事后再执行一遍还是需要改变B+树的结构. 所以物理日志比二进制日志要恢复得快
2>undo
重做日志记录了事务的行为, 可以很好地通过其对页进行"重做"操作. 但是事务有时还要进行回滚操作, 这时需要undo. 因此在对数据库 进行修改时, InnoDB不但会产生redo , 还会产生一定量的undo.这样如果用户执行的事务或者语句由于某种原因失败了, 又或者用户一条ROLLBACK语句请求回滚, 就可以利用这些undo信息将数据回滚到修改之前的样子
redo存放在重做日志文件中, 与redo不同, undo存放在数据库内部的一个特殊段中, 这个段称之为undo段. undo段位于共享表空间里
用户对undo通常有误解 : undo用于将数据库物理的恢复到执行语句或事务前的样子. 但实际上, undo是逻辑日志, 只是将数据库逻辑地恢复到原来的样子.所有修改的逻辑都被取消了, 但是数据结构和页本身在回滚之后可能大不相同.
除了回滚操作, undo的另一个作用是MVCC, 即 在InnoDB存储引擎中MVCC的实现是通过undo来完成. 当用户读取一行记录时, 若该记录已经被其他事务占用, 当前事务可以通过undo读取之前的行版本信息, 以此实现非锁定读取
最后也是最重要的一点是, undo log也会产生redo log, 也就是undo log的产生会伴随redo log的产生, 这是因为undo log也需要持久性的保护
undo存储管理:
事务在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页会非常浪费存储空间, 因为事务提交 时, 可能不能马上释放页. 因此, InnoDB设计中对undo页可以进行重用. 具体来说, 当事务提交时, 首先将undo log 放入链表中, 然后判断undo页的使用空间是否小于3/4, 若是则表示undo页 可以被重用.之后新的undo log记录在当前undo log的后面, 由于存放undo log的列表是以记录进行组织的, 而undo页可能存放着不同事务的undo log, 因此purge需要涉及磁盘的离散读取操作, 是一个比较缓慢的过程
undo log格式 :
InnoDB中, undo log 分为 insert undo log和update undo log
insert undo log是指在insert操作中产生的undo log. 因为insert操作产生的 记录, 只事务本身可见, 对其他事务不可见, 故 该undo log在事务提交后可以直接删除
update undo log记录的是对delete和update操作产生的undo log. 该undo log可能需要提供MVCC机制, 因此不能在事务提交时就删除, 提交时放入undo log链表, 等待purge线程最后进行最后的删除
purge :
delete和update操作可能并不直接删除原有的数据. purge用于最终完成delete和update操作. 这样设计是因为 InnoDB支持MVCC, 所以记录不能在事务提交时立即进行处理. 这时其他事务可能正在引用这行, 故InnoDB存储引擎需要保存记录之前的版本. 而是否可以删除该条记录通过purge来进行判断. 若该行记录已经不被热河其他事务引用, 那么就可以进行真正的delete操作. 可见, purge操作是清理之前的delete和update操作, 将上述操作"最终完成". 而实际执行的操作为delete操作, 清除之前行记录的版本
在进行purge操作时, 会存在一个 history list, 记录提交了的事务. 最先提交的事务位于尾部. 在进行purge操作时, 先从尾部找到事务trx1, 清理之后, 接着在trx1的undo log所在的页汇总继续寻找是否存在可以清理的记录, 如果该记录 被其他事务引用, 则不能清理, 以此类推, 这种从history list中找undo log, 再从undo page中找undo log的设计模式是为了避免大量的随机读取操作, 从而提高purge的效率
group commit :
若事务非只读事务, 每次提交事务时需要进行一个fsync操作, 以此保证重做日志文件都已经写入了磁盘. 当数据库发生宕机时, 可以通过重做日志文件恢复. 磁盘的fsync性能是有限的, 当前数据库提供了group commit的功能, 即以此fsync操作确保多个事务日志被写入文件. 对于InnoDB来说, 事务提交是会进行两个阶段的动作 :
- 修改内存中事务对应的信息, 并且将日志写入重做日志缓冲
- 调用fsync将确保日志都从重做日志缓冲写入磁盘
有了group commit, 可以将多个事务的重做日志通过一次fsync操作就刷新到磁盘, 这样就大大减小了磁盘的压力
Binary Log Group Commit(BLGC) :
BLGC实现方式是将事务提交的过程分为几个步骤来完成
在MySQL数据库上层进行提交时首先按照顺序将其放入一个队列中, 队列中的第一个 事务叫做leader, 其他事物称为follower, leader控制着flowwer的行为
- Flush阶段 , 将每个事务的二进制日志写入内存中
- Sync阶段, 将内存中的二进制日志刷新到磁盘, 若队列中有多个事务, 那么一次fsync操作就完成了二进制日志的写入
- Commit阶段, keader根据顺序调用存储引擎事务的提交
当有一组事务在进行 Commit阶段时, 其他事务 可以进行Flush阶段
4.事务的隔离级别
SQL标准定义的四个隔离级别分别为:
- READ UNCOMMITED
- READ COMMITED
- REPEATABLE READ
- SERIALIZABLE
InnoDB的默认隔离级别为REPEATABLE READ, 但是与标准SQL不同的是, InnoDB存储引擎在REPEATABLE READ事务隔离级别下, 使用Next-Key Lock锁的算法, 因此避免幻读的产生. 但是其会出现丢失 更新的现象, 若要避免丢失更新, 还是需要SERIALIZABLE的隔离级别