MySQL InnnoDB 存储引擎事务
事务(Transaction)是数据库区别于文件系统的重要特性之一。事务会把数据库从一种一致状态转换为另一种一致状态。
事务可由一条非常简单的 SQL 语句组成,也可以由一组复杂的 SQL 语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做。
事务的概念
InnoDB 存储引擎中的事务完全符合 ACID 的特性:
-
原子性(Atomicity)
原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个 SQL 语句执行失败,已经执行成功的 SQL 语句也必须撤销,数据库状态应该退回到执行事务前的状态。 -
一致性(Consistency)
一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏。 -
隔离性(Isolation)
隔离性又叫做并发控制(concurrency control)
、可串行化(serializability)
、锁(locking)
等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。由定义可见,一般用于出发数据的的并发请求。 -
持久性(durability)
事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。但是只能从事务本身的角度来保证结果的永久性,一些外部原因的故障导致的数据丢失是无法保障的。因此持久性保证事务系统的高可靠性(High Reliability)
,而不是高可用性(High Availability)
事务的实现
事务的隔离性由锁来实现,关于 InnoDB 存储引擎中的锁,可以参考本人之前的一篇文章。
原子性、一致性、持久性通过数据库的 redo log 和 undo log 来完成。redo log 称为重做日志,用来保证事务的原子性和持久性。undo log 用来保证事务的一致性。
redo
redo log 用来实现事务的持久性,记录的是对于每个页的修改。
其由两部分组成:
- 内存中的重做日志缓冲(redo log buffer),因为是存在于内存中,其是易失的;
- 重做日志文件(redo log file),其是持久的
InnoDB 存储引擎通过 Force Log at Commit
机制来实现事务的持久性,即当事务提交(COMMIT)使,必须先讲该事务的所有日志写入到重做日志文件进行持久化,待事务的 COMMIT 操作完成才算完成。为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB 存储引擎都需要调用一次 fsync 操作(确保重做日志从文件系统缓存写入到磁盘)。
与 binlog 的区别
接触过 MySQL 的同学应该多少都会了解 binlog
,会发现 binlog 与 redo log 非常相似,都是记录了对于数据操作的日志,为什么还要引入 redo log?binlog的两个重要使用场景:
- MySQL 主从复制
- 数据恢复
但是两者是存在非常大的区别的:
- redo log 是在 InnoDB 存储引擎层产生,binlog 是在 MySQL 数据库的上层产生,不仅仅针对 InnoDB 存储引擎
- 内容形式不同。binlog 是一种逻辑日志,记录的是对应的 SQL 语句或行的内容;redo log 是物理格式日志,记录的是对于每个页(InnoDB 数据的存储格式)的修改
- 写入磁盘的时间点不同。binlog 只在事务提交完成后进行一次写入;redo log 在事务进行中不断地被写入。
- redo log 空间是固定的,可重用;binlog 是追加写,当前文件写完之后会开启一个新文件继续写。
InnoDB 存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。redo log 记录的是物理日志,恢复速度比 binlog 要快很多,而且InnoDB 恢复还进行了一定程度的优化-顺序读取及并行应用redo log。
group commit
事务的两阶段提交:
- 修改内存中事务对应的信息,并且将日志写入重做日志缓冲
- 调用 fsync 确保日志都从重做日志缓冲写入磁盘
若事务为非只读事务,则每次事务提交时需要进行一次 fsync 操作,以此保证重做日志已经写入磁盘。
因为磁盘的 fsync 性能时有限的,即上面的第二阶段是个较慢的过程。但是当有事务进行第二阶段提交时,其他事务可以进行第一阶段的提交,正在提交的事务完成提交操作后,再次进行第二阶段,即 group commit ,一次 fsync 可以刷新确保多个事务日志被写入文件。
但是开启 binlog 之后,group commit 功能就会失效。这是因为开启 binlog 之后,需要保证binlog 和 redo log 的一致性,二者之间使用了两阶段事务,步骤如下:
- 事务提交时 InnoDB 存储引擎进行 prepare 操作
- MySQL 数据库上层写入 binlog
- InnoDB 存储引擎将日志写入 redo log
- 写入redo log 缓冲
- 调用 fsync
开启 binlog 后 InnoDB 存储引擎的提交过程.png
为了保证 binlog 的写入顺序和 事务提交顺序一致,MySQL 内部使用了prepare_commit_mutex
锁,启用该锁后,步骤3.1 不可以在其他事务执行 3.2 时进行,从而导致了 group commit 失效。
MySQL 5.6 采用 Binary Log Group Commit(BLGC) 来提高日志的写入性能:
在 MySQL 数据库上层进行提交时首先按顺序将其放入一个队列中,队列中的第一个事务称为 leader,其他事务称为 follower,leader 控制着 follower 的行为。
- Flush 阶段,将每个事务的 binlog 写入内存中
- Sync 阶段,内存中的 binlog 刷新到磁盘(如果队列中有多个事务,一次 fsync 操作就完成了多个 binlog 的写入)
- Commit 阶段,leader 根据顺序调用存储引擎层事务的提交,InnoDB 可以继续使用 group commit
undo
undo log 用于将数据回滚到修改之前的样子,因为回滚做的事情实际上是与之前相反的工作。所以,对于 INSERT ,undo log 会对应一个 DELETE 记录;对于 DELETE,画丶do log 对应一个 INSERT;对于 UPDATE,undo log 对应一个相反的 UPDATE。
undo log 另一个作用是 MVCC,当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过 undo 读取之前的行版本信息,从而实现 一致性非锁定读。
undo 存储管理
InnoDB 存储引擎对 undo 的管理采用段(segment)
的方式,rollback segment 称为回滚段,每个回滚段记录了 1024 个 undo log segment。
在 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 线程进行最后的删除。
事务提交后并不能马上删除 undo log 及 undo log 所在的页。因为可能还有其他事务需要通过 undo log 来得到行记录之前的版本。事务提交时将 undo log 放入一个链表中,是否可以最终删除 undo log 及 undo log 所在页由 purge 线程来判断。并且提交事务时,还会判断 undo log 页是否可以重用,如果可以重用,则会分配给后面的事务。
purge
purge 操作用于最终完成 delete
和 update
操作。
InnoDB 存储引擎有一个 history 列表,根据事务提交的顺序,将 undo log 进行链接。InnoDB 存储引擎先从 history list 中找 undo log,然后再从 undo page 中找 undo log,避免大量的随机读取操作,从而提高 purge 的效率。
redo log 用来保证事务的持久性,undo log 用来帮助事务回滚以及 MVCC 的功能。redo log 基本上都是顺序写的,在数据运行时不需要对 redo log 的文件进行读取操作,而 undo log 是需要进行随机读取的。