MySQL系列之事务及其实现

2021年9月1日 138点热度 1人点赞 0条评论

事务概述

事务是数据库中的一个重要概念,也是数据库系统区别于文件系统的重要特性之一。事务是访问并更新数据库中各种数据项的一个程序执行单元。事务可由一条SQL语句组成,也可以由多条SQL语句组成。事务会把数据库从一种一致状态转换到另一种一致状态。在数据库提交工作时,可以确保要么所有的修改都已经完成了,要么所有的修改都没有做,并且保存了事务开始前的状态,以便可以回到开始的状态。

事务包含四个(ACID)特性:

  • 原子性(atomicity)

原子性是指事务是一个不可分割的执行单元。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句都必须撤销,数据库状态应该退回到执行事务前的状态。

  • 一致性(consistency)

一致性是指事务将数据库从一种一致性状态转换到另一种一致性状态。在事务开始前和事务结束后,数据的完整性约束没有被破坏。事务是一致性的单位,如果事务中某个工作失败了,系统可以自动撤销事务,也就是返回初始状态。

  • 隔离性(isolation)

隔离性还有其他称呼,如并发控制,可串行化,锁等。事务的隔离性要求每个读写事务的对象对其他事物的操作对象能相互分离,即该事务提交前对其他事务都是不可见的,通常这使用锁来实现。

  • 持久性(durability)

事务一旦提交,其结果就是永久的,即使发生宕机等故障,数据库也能将事务恢复。持久性保证了事务系统的高可靠性,但不能保证事务系统的高可用性。

事务的实现

事务隔离性是由锁来实现的,原子性,一致性,持久性由 redo log 和 undo log 来完成。redo log 称为重做日志,用来保证事务的原子性和持久性。undo log 来保证事务的一致性。

redo 和 undo 的作用都可以看作是一种恢复操作,redo 恢复提交事务修改的页操作,而 undo 回滚行记录到某个特定版本。因此两者记录的内容是不同的,redo 通常是物理日志,记录的是页的物理修改操作;undo 是逻辑日志,根据每行记录进行记录。

这里关于事务隔离性的实现可以见链接:
MySQL系列之事务的隔离级别及其实现

redo

基本概念

重做日志用来实现事务的持久性。redo 由两部分组成,一部分是内存中的重做日志缓冲,其是易失的;另一部分是重做日志文件,是持久的。

InnoDB存储引擎是事务的存储引擎,其通过 Force Log At Commit机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件中进行持久化,然后事务的 commit 操作才算完成。这里的日志由两部分组成,即 redo log 和 undo log 。redo log 是来保证事务的持久性,undo log 是来帮助事务进行回滚及MVCC功能。redo log 基本上都是顺序写的,在数据库运行时不需要对 redo log 进行读取操作。而 undo log 是要进行随机读写的。为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次 fsync 操作。

在MySQL中还有一种二进制日志(binlog),其用来进行point-in-time的恢复及主从复制环境的搭建。

binlogredo log是有很大不同的。首先,重做日志是在InnoDB存储引擎层产生的,而二进制日志时在MySQL数据库上层的一种逻辑日志,其记录的是对应的SQL语句;而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对每个页的修改。此外,两种日志的刷盘的时间点不同,二进制日志只在事务提交完成后进行一次写入,而 redo log 在事务进行中不断地进行写入,并在事务提交之前完成落盘,以此才能保证事务的持久性。

log block

在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓冲、重做日志文件都是以块的方式进行保存的,称之为重做日志块。每块的大小为512字节。

若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,所以重做日志的写入是可以保证原子性的,不需要doublewrite技术。重做日志除了日志本身记录的内容以外,还有日志头和日志块尾两部分组成,头占12字节,尾占8字节,故每个重做日志块存储的实际内容为492字节。重做日志块缓存的结构如下图所示:

重做日志文件中存储的就是之前在 log buffer 中的 log block,因此其也是根据块的方式进行物理存储管理的。在InnoDB存储引擎运作过程中,log buffer 根据一定规则将内存中的 log block 刷新到磁盘中:

  • 事务提交时;
  • 当 log buffer 中有一半的内存空间已经被使用时;
  • log checkpoint 时;

重做日志格式

重做日志的通用格式为:

redo_log_type space age_no redo log body

其中,redo_log_type为重做日志的类型;space为表空间的ID;page_no为页的偏移量。

LSN

LSN是Log Sequence Number的缩写,其代表的是日志序列号。在InnoDB存储引擎中,LSN占用8字节,并且单调递增。

LSN表示的含义有:

  • 重做日志写入的总量
  • checkpoint的位置
  • 页的版本

恢复

InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。由于重做日志记录的是物理日志,因此恢复的速度比逻辑日志,比如二进制日志,要快很多。而且,InnoDB存储引擎自身也对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步地提高数据库恢复的速度。

由于checkpoint表示已经刷新到了磁盘页上的LSN处,因此在恢复过程中仅需恢复checkpoint开始的日志部分。比如,如果数据库在checkpoint的LSN为10000时开始宕机,恢复操作仅恢复LSN在10000以后的日志。

undo

基本概念

重做日志记录了事务的行为,可以很好地通过其对页进行重做操作。但是事务有时还需要回滚操作,这就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不仅会产生redo,还会产生一定量undo。这样,当用户执行的事务或者语句由于某种原因失败了,又或者用户用一条rollback语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。

redo存放在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段中,这个段称为undo段,undo段是位于共享表空间中的。

用户通常对undo有这样的误解:

undo用于将数据库物理地恢复到执行语句或事务之前的样子,这是错误的。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚后可能大不相同。这是因为在多用户并发系统中,可能会有数十,数百个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有另一个事务在对同一页中的另外几条记录进行修改,因此不能将一个页物理回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。

举个例子:
一个用户执行了一个insert 10w条数据的事务,这个事务会导致分配一个新的段,即表空间会增大,在用户执行rollback之后
会将插入的事务进行回滚,但是表空间不会收缩,因此当进行回滚时,它实际上做的是与之前相反的工作,对于每个insert,它会来一个delete,来一个update,InnoDB存储引擎会来一个与之相反的update。

除了回滚操作,undo的另一个作用就是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo来读取之前的行版本信息,以此来实现非锁定读。

最后也是最为重要的一点,undo也会产生redo,也就是undo的产生会伴随着redo的产生,因为undo同样需要redo提供的持久化。

undo存储管理

InnoDB存储引擎对undo的管理同样采用段的方式。首先,InnoDB存储引擎有rollback segment,每个回滚段记录了1024个undo log segment,而在每个undo log segment段中进行undo页的申请。共享表空间偏移量为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线程来判断。

为了避免浪费存储空间,在InnoDB存储引擎的设计中对undo页可以进行重用。具体来说,当事务提交时,首先将undo log放入链表中,然后判断undo页的使用空间是否小于3/4,若是则表示该undo页可以被重用,之后新的undo log记录在当前undo log的后面。由于存放undo log的列表是以记录进行组织的,而undo页可能存放着不同事务的undo log,因此purge操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程。

undo log格式

undo log有两种,一种是insert undo,一种是update undo。

insert undo是指在insert操作中产生的undo,因为insert操作的记录,只对事务本身可见,对其他事务不可见,这是事务隔离性的要求,所以这种undo log可以在事务提交以后直接删除。不需要进行purge操作。

update undo是指update和delete操作产生的undo,该undo可能需要提供MVCC机制,因此不能在事务提交以后就删除,提交时放入undo链表,等待purge线程进行最后的删除。

insert undo格式如下左图,update undo格式如下右图:

purge

delete和update操作可能并不直接删除原有的数据,只是将这条记录的delete flag设置为1,记录并没有被删除,即记录还在B+树中。而真正的删除操作延时在了purge操作中完成。

purge用于最终完成delete和update操作。这样设计是因为InnoDB存储引擎支持MVCC机制,所以记录不能在事务提交时立即进行处理。这时其他事务可能正在引用这行,故InnoDB存储引擎需要保存记录之前的版本。而是否可以删除这条记录需要由purge进行判断,若该行记录不再由任何其他事务引用,那么就可以真正进行delete操作。可见,purge是用来清理之前的delete和update操作,将这些操作最终完成。

根据上面所述,undo的设计决定了一个页上允许多个事务的undo log存在,并且后面的事务产生的undo log总在最后。此外,InnoDB存储引擎还有一个history列表,它根据事务提交的顺序,将undo log进行了链接。

在执行purge的过程中,InnoDB存储引擎首先从history list中找到第一个需要清理的记录,清理之后InnoDB存储引擎会在这个记录的undo log所在的页中继续寻找是否存在可以被清理的页,若果有就直接清理。然后再去history list中寻找需要清理的记录,循环这个过程,知道清理完毕。

group commit

若事务为非只读事务,则每次在事务提交时需要进行一次fsync操作,以此保证重做日志都已经写入磁盘。当数据库发生宕机时,可以通过重做日志进行恢复。虽然固态硬盘的出现提高了磁盘的性能,然而磁盘的fsync性能是有限的。为了提高磁盘fsync的效率,当前数据库都提供了group commit的功能,即一次fsync可以刷新确保多个事务日志被写入文件。对于InnoDB存储引擎来说,事务提交会进行两个阶段的操作

  • 修改内存中事务对应的信息,并且将日志写入重做日志缓冲;
  • 调用fsync确保日志都从重做日志缓冲写入到磁盘中;

第二个步骤相对于第一个步骤比较慢,当有事务进行步骤二的时候,其他事务可以进行步骤一,正在提交的事务完成提交操作后,再次进行操作二时,可以将多个事务的重做日志通过一次fsync刷新到磁盘,这样就大大减少了磁盘的压力,从而提高了数据库的整体性能。

agedcat_xuanzai

这个人很懒,什么都没留下

文章评论