InnoDB存储引擎事务ACID的实现

InnoDB存储引擎对事务有着良好的支持,完全符合ACID的特性,支持以下几种事务类型:

  • 扁平事务
  • 带有保存点的事务
  • 链事务
  • 分布式事务

InnoDB不支持嵌套事务,用户可通过带有保存点的事务来模拟串行的嵌套事务。

InnoDB对事务ACID的支持由多种机制实现:

  • 事务隔离性由锁来实现,包含表锁、行锁
  • 原子性、持久性由InnoDB的redo log(重做日志)来完成,重做日志负责恢复
  • 一致性由undo log来实现
Redo Log

重做日志用来实现事务的持久性,由两部分组成

  • redo log buffer(重做日志缓冲),存在于内存中
  • redo log file(重做日志文件),存在于磁盘上

InnoDB在提交事务时,采用Force Log at Commit机制保证持久性:首先要先将该事务的所有日志写入到重做日志文件,并调用fsync操作,待事务的COMMIT操作完成时才算完成事务。

Redo Log是顺序写的,数据库运行期间无需对其进行读取操作。

参数innodb_flush_log_at_trx_commit可以控制重做日志刷新磁盘的策略,默认值为1,表示事务提交时必须调用fsync操作,这样即使是操作系统发生了宕机也不会影响事务的持久性。此外还可以将其设置为0和2。当值为0时表示事务提交时不进行写入重做日志的操作,这么设置的话相当于失去了事务的持久性。设置为2时表示事务提交时将重做日志写入到重做日志文件,但不进行显式的fsync操作,这样可以在操作系统不发生崩溃、宕机的前提下保证事务的持久性。

总的来说,将innodb_flush_log_at_trx_commit设置为0或2可以有效地提高InnoDB表写入的性能,但是事务的持久性得不到保证。

更多有关重做日志的内容可以参考这篇博客

Undo Log
基本概念

事务的回滚操作是基于undo log实现的,在事务中,除了会持续写入重做日志,还会产生undoundo存放在数据库内部的一个特殊段(segment)中,这个段称之为undo段,这个段存放于共享表空间中。

undo是逻辑日志,InnoDB根据该日志逻辑地将数据库内容恢复到事务开始时的样子,因此数据结构和页本身在回滚之后可能大不相同。InnoDB在实际生产环境中可能要处理成百上千个并发事务,可能就会有别的事务对同一个页中另外几行数据进行修改,因此不能将一个页回滚到事务开始前的样子。例如:用户在一个事务中执行了多个INSERT命令后,表空间可能会因此增大,当用户回滚该事务时,表空间并不会因此而变小,它的回滚操作执行的是相反的操作,例如INSERT改为DELETEUPDATE改为相反的UPDATE

除了用于回滚操作,undo log还被用来实现MVCC。MVCC用于保障事务的隔离性:当用户读取一行记录时,如果该记录已经被其他事务修改并且尚未提交,那么该用户可通过undo读取该事务开始之前的行数据。

undo作为表空间中的一个段,它的产生同样也会伴随着重做日志的产生,因为undo也是需要保证其持久性的。

存储管理

在共享表空间中,每个rollback segment记录了1024个undo log segment,在每个undo log segment中进行undo页的申请。共享表空间偏移量为5的页记录了所有的rollback segment header所在的页,这个页的类型为FIL_PAGE_TYPE_SYS

InnoDB存储引擎默认支持128个rollback_segment,所以支持同时在线的事务限制提高到了128 * 1024。
InnoDB支持以下undo相关的参数

  • innodb_undo_directory,用于设置rollback_segment文件所在路径,所以rollback segment可以存放在独立表空间中。默认值为.
  • innodb_undo_logsrollback segment的数量上限,默认128
  • innodb_undo_tablespaces,构成rollback segment的文件数量,可以将rollback segment拆分为多个文件。默认为3。
  • innodb_undo_log_truncate,是否开启在线回收(收缩)undo log,默认为OFF。
mysql> show variables like 'innodb_undo_%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_undo_directory    | .\    |
| innodb_undo_log_truncate | OFF   |
| innodb_undo_logs         | 128   |
| innodb_undo_tablespaces  | 0     |
+--------------------------+-------+
4 rows in set

事务提交时,InnoDB会完成以下两个事件:

  1. undo log放入列表,供之后的purge操作。这是因为事务提交后不能立即删除undo logundo log所在页。因为可能还会有其它事务需要通过undo log来获得旧版本的行记录,所以将其放置于列表,最终能否删除由purge线程决定。
  2. 判断undo log所在的页是否能够重用,若可以分配给下个事务使用。因为为每个事务分配一个单独的undo页很浪费空间。具体实现是,在undo log放入链表后,然后判断undo页的使用空间是否小于75%,如果是则表示可以继续被重用,之后新的undo log记录在当前undo log后面。由于存放undo log的列表是以记录的形式组织的,undo页也存放着不同事务的undo log,因此purge操作需要涉及到硬盘的随机读取操作,相对来说较缓慢。
格式

Undo Log分为两种类型:

  • insert undo log:是指在insert操作中产生的undo log。因为事务隔离性的要求:insert本身的记录只能对事务本身可见,对其它事务不可见,故该undo log可以在事务提交后直接删除,无需进行purge操作。
  • update undo log:记录的是对deleteupdate操作产生的undo log。该undo log需要提供MVCC机制,因此不能在事务提交后就删除。所以需要在提交时放入undo log链表,等待purge线程进行处理。

下面是insert undo logupdate undo log的结构图(*表示对字段进行了压缩):
InnoDB存储引擎事务ACID的实现_第1张图片
insert undo log前2个字节next记录了下一个undo log的位置,尾部两个字节记录了当前undo log开始的位置。type_cmpl占用1个字节,记录了undo类型,对于insert undo log,其值为11。undo no记录了事务ID,table id记录了undo log所对应的表。接着的部分记录了所有主键的列和值,进行回滚操作时根据这些记录可以快速定位到具体的行,并进行删除。

update undo log的结构比insert undo log复杂,记录的内容更多,所需空间也就更大。nextstartundo notable id的意义和insert undo log相同。至于type_cmpl,因为update undo log还可以划分为3种类型,其值有所不同:

  • 12,即TRX_UNDO_UPD_EXIST_REC,更新non-delete-mark的记录
  • 13,即TRX_UNDO_UPD_DEL_REC,将delete记录标记为not delete
  • 14,即TRX_UNDO_DEL_MARK_REC,将记录标记为delete

update_vector表示update操作导致发生改变的列。每个修改的列信息都要记录在undo log中。

Purge

为了方便演示,我们建立一个表t,并插入一些数据:

CREATE TABLE t (a INT, b INT, PRIMARY KEY(a), KEY(b))
INSERT INTO t SELECT 1, 1
INSERT INTO t SELECT 3, 1
INSERT INTO t SELECT 5, 3
INSERT INTO t SELECT 7, 6
INSERT INTO t SELECT 10, 8

其中列a为主键,并给列b添加了辅助索引。
接下来执行一个删除命令:

DELETE FROM t WHERE a = 1

该命令会将主键列等于1的记录delete flag设置为1,此时记录并没有真正被删除,依然存在于聚簇索引中,辅助索引中的记录同样存在。真正执行删除的是purge线程。

purge线程被设计成用于完成deleteupdate操作。因为InnoDB支持MVCC机制,所以记录不能在事务提交后立刻处理,因为其它事务可能正在引用这行,需要保存该行的旧版本记录。只有该行不被其他任何事务引用,那么才能执行真正的delete操作。

参数innodb_purge_batch_size可以设置每次purge操作需要清理的undo页数量:

mysql> show variables like 'innodb_purge_batch_size';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_purge_batch_size | 300   |
+-------------------------+-------+
1 row in set

该值默认为300。一般来说,该值设置得越大,每次purge操作回收的undo页页就越多,这样可重用的undo页就越多,减少了磁盘存储空间使用和分配过程造成的时间开销。如果设置得过大,那么可能会导致数据库过于集中对于undo log的处理,造成性能的下降。一般来说普通用户无需调整该参数。

Group Commit

默认情况下,非只读事务在提交时都需要进行一次fsync操作。为了提高fsync的效率,InnoDB提供了group commit机制,即一次fsync操作就可以确保将多个事务的重做日志写入到文件,具体来说分为两个步骤:

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

fsync是一个较为缓慢的操作,因为涉及到了磁盘IO的性能。通过合并多个事务的重做日志然后仅调用一次fsync操作来完成重做日志的持久化,是提高数据库性能的有效方式,能够大大减少磁盘的压力。

MySQL 5.6之后的版本采用了称之为Binary Log Group Commit(BLGC)的方式来实现group commit。在提交事务时,首先按顺序将事务放到一个队列中,队列中的第一个事务称之为leader,其它事务称之为followerleader控制follower的行为。
BLGC将事务提交的过程分为了三个阶段:

  1. Flush阶段,将每个事务的二进制日志写入到内存中(二进制日志缓冲)
  2. Sync阶段,将二进制日志缓冲内容刷新到磁盘,若队列有多个事务,那么仅调用一次fsync就能够完成二进制日志的写入,这就是BLGC
  3. Commit阶段,leader事务根据顺序调用存储引擎层事务的提交(InnoDB本身支持group commit,也就是上面那两个步骤)。

当有一组事务进行Commit的同时,其它事务也可以进行Flush操作,从而使group commit不停地生效。group commit的性能提升效果由队列中的事务数量决定。如果每次事务队列中仅有一个事务,那么性能可能反而会变差。

参考资料

《MySQL技术内幕(InnoDB存储引擎)》

你可能感兴趣的:(MySQL,MySQL,InnoDB,数据库,事务)