InnoDB存储引擎对事务有着良好的支持,完全符合ACID的特性,支持以下几种事务类型:
InnoDB不支持嵌套事务,用户可通过带有保存点的事务来模拟串行的嵌套事务。
InnoDB对事务ACID的支持由多种机制实现:
redo log
(重做日志)来完成,重做日志负责恢复undo 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
。undo
存放在数据库内部的一个特殊段(segment)中,这个段称之为undo
段,这个段存放于共享表空间中。
undo
是逻辑日志,InnoDB根据该日志逻辑地将数据库内容恢复到事务开始时的样子,因此数据结构和页本身在回滚之后可能大不相同。InnoDB在实际生产环境中可能要处理成百上千个并发事务,可能就会有别的事务对同一个页中另外几行数据进行修改,因此不能将一个页回滚到事务开始前的样子。例如:用户在一个事务中执行了多个INSERT
命令后,表空间可能会因此增大,当用户回滚该事务时,表空间并不会因此而变小,它的回滚操作执行的是相反的操作,例如INSERT
改为DELETE
、UPDATE
改为相反的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_logs
,rollback segment
的数量上限,默认128innodb_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会完成以下两个事件:
undo log
放入列表,供之后的purge
操作。这是因为事务提交后不能立即删除undo log
和undo log
所在页。因为可能还会有其它事务需要通过undo log
来获得旧版本的行记录,所以将其放置于列表,最终能否删除由purge
线程决定。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
:记录的是对delete
和update
操作产生的undo log
。该undo log
需要提供MVCC机制,因此不能在事务提交后就删除。所以需要在提交时放入undo log
链表,等待purge
线程进行处理。下面是insert undo log
和update undo log
的结构图(*
表示对字段进行了压缩):
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
复杂,记录的内容更多,所需空间也就更大。next
、start
、undo no
、table id
的意义和insert undo log
相同。至于type_cmpl
,因为update undo log
还可以划分为3种类型,其值有所不同:
TRX_UNDO_UPD_EXIST_REC
,更新non-delete-mark
的记录TRX_UNDO_UPD_DEL_REC
,将delete
记录标记为not delete
TRX_UNDO_DEL_MARK_REC
,将记录标记为delete
update_vector
表示update
操作导致发生改变的列。每个修改的列信息都要记录在undo log
中。
为了方便演示,我们建立一个表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
线程被设计成用于完成delete
和update
操作。因为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
的处理,造成性能的下降。一般来说普通用户无需调整该参数。
默认情况下,非只读事务在提交时都需要进行一次fsync
操作。为了提高fsync
的效率,InnoDB提供了group commit
机制,即一次fsync
操作就可以确保将多个事务的重做日志写入到文件,具体来说分为两个步骤:
fsync
确保日志都从重做日志缓冲写入到了磁盘fsync
是一个较为缓慢的操作,因为涉及到了磁盘IO的性能。通过合并多个事务的重做日志然后仅调用一次fsync
操作来完成重做日志的持久化,是提高数据库性能的有效方式,能够大大减少磁盘的压力。
MySQL 5.6之后的版本采用了称之为Binary Log Group Commit
(BLGC)的方式来实现group commit
。在提交事务时,首先按顺序将事务放到一个队列中,队列中的第一个事务称之为leader
,其它事务称之为follower
。leader
控制follower
的行为。
BLGC将事务提交的过程分为了三个阶段:
fsync
就能够完成二进制日志的写入,这就是BLGCleader
事务根据顺序调用存储引擎层事务的提交(InnoDB本身支持group commit
,也就是上面那两个步骤)。当有一组事务进行Commit的同时,其它事务也可以进行Flush操作,从而使group commit
不停地生效。group commit
的性能提升效果由队列中的事务数量决定。如果每次事务队列中仅有一个事务,那么性能可能反而会变差。
《MySQL技术内幕(InnoDB存储引擎)》