深入理解InnoDB -- 事务篇

事务的定义

事务的基本要素(ACID)
原子性:Atomicity,整个数据库事务是不可分割的工作单位
一致性:Consistency,事务将数据库从一种状态转变为下一种一致的状态
隔离性:Isolation,每个读写事务的对象对其他事务的操作对象能相互分离
持久性:Durability,事务一旦提交,其结果是永久性的

事务的并发问题
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
不可重复读:事务 A 多次读取同一数据,期间事务 B对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
幻读:事务 A 多次同一条件查询,期间事务B删除或插入了满足条件的数据,导致事务A多次读取的结果集不一致。

SQL标准定义的隔离级别为:

事务隔离级别 描述 脏读 不可重复读 幻读
READ UNCOMMITTED 一个事务会读到另一个未提交事务修改过的数据。
READ COMMITTED 一个事务只能读到另一个已经提交的事务修改过的数据。
REPEATABLE READ 一个事务只能读到另一个已经提交的事务修改过的数据,而且该事务第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值。
SERIALIZABLE 事务串行化执行

InnoDB默认支持REPEATABLE READ,但与标准SQL不同,InnoDB在REPEATABLE READ事务隔离级别下,使用Next-Key Lock算法,避免了幻读问题。

事务的实现

redo log

redo log称为重做日志,保证事务的原子性,持久性

当前事务数据库系统普遍采用Write Ahred Log策略(WAL,预写式日志),即当事务提交时,先写重做日志,再修改磁盘页数据。如果宕机,则通过重做日志来完成数据的恢复。这也是事务ACID中D(Durability持久性)的要求。
实际上是最先操作是修改内存池中的页数据。先写重做日志是相对于修改磁盘数据而言。

任何对Innodb表的更新,Innodb都会将更新操作转化为redo log并写入磁盘,redo log中记录了修改的详细信息。

redo log是物理日志,记录页的偏移量和字节等数据。
重做redo log,实际是重做提交事务修改的页物理操作。

innodb_flush_log_at_trx_commit参数控制重做日志缓存刷新到磁盘的策略。
1:默认,事务提交必须调用一次fsync操作
0:事务提交时不进行写入重做日志操作,由master thread每1秒进行一次fsync操作
2:事务提交时仅将重做日志写入文件系统缓存,不进行fsync操作,当mysql宕机而操作系统不宕机时,不会导致事务丢失。

除了事务提交时,刷新重做日志到磁盘还有如下场景

  1. redo log buffer已经使用一半内存空间

  2. Async/Sync Flush Checkpoint

log block

重做日志缓冲,重做日志文件都是以块(block)的方法进行保存的,称为重做日志块(redo log block),每块的大小为512字节。重做日志块与磁盘扇区大小一样,因此重做日志的写入可以保证原子性,不需要doublewrite技术。

日志块由三部分组成,依次为日志块头(log block header),日志内容(log body),日志块尾log block tailer

日志块头内容以下

变量 字节 描述
LOG_BLOCK_HDR_NO 4 log buffer由log block组成,在内部log buffer就好似一个由log block数组,LOG_BLOCK_HDR_NO用于标记这个数组的下标,递增并且循环使用。第一位用做flush bit,最大值为2G
LOG_BLOCK_HDR_DATA_LEN 2 表示log block占用的大小。最大为0x200,表示log block512字节
LOG_BLOCK_FIRST_REC_GROUP 2 新事务第一个日志偏移量。如果log block中存储了事务L1后还有空余空间,还会存储下一个事务L2的内容,LOG_BLOCK_FIRST_REC_GROUP记录事务L2的偏移量。

若LOG_BLOCK_FIRST_REC_GROUP与LOG_BLOCK_HDR_DATA_LEN相同,表示当前log block不含新的日志
LOG_BLOCK_HDR_NO是通过lsn计算得到的,因此InnoDB也可以通过lsn定位到具体的redo log。

日志块尾只有LOG_BLOCK_TRL_NO,4字节,与LOG_BLOCK_CHECKPOINT_NO保持一致

log group

重做日志组是一个逻辑概念,由多个重做日志文件redo log file组成,每个log group中的日志大小是相同的。默认重做日志组由ib_logfile0,ib_logfile1组成。
InnoDB 1.2 版本前,重做日志组的总大小要小于4GB,InnoDB 1.2 版本开始,提高到512GB

对log block的写入追加在redo log file的最后部分,当一个redo log file被写满时,会接着写入下一个redo log file,使用方式为round-robin。
架构篇说过,当页被Checkpoint刷新到磁盘后,对应的重做日志就不需要了 ,其空间可以被覆盖重用。

每个redo group的第一个redo log file中前2kb不保存log block的信息,而是保存log file header,checkpoint信息。
redo log使用顺序写,速度很快,但checkpoint后,需要更新第一个日志文件的头部checkpoint标记,这时并不是顺序写。
redo group非第一个redo log file中前2KB内容并不存储信息,预留为空。

redo log file前2k内容,依次存放以下数据

属性 字节
LOG FILE HEADER 512
CHECKPOINT BLOCK1 512
512
CHECKPOINT BLOCK2 512

重点看一下CHECKPOINT BLOCK的内容

属性 字节 解析
LOG_CHECKPOINT_NO 8 单调递增的值,每次checkpoint操作完成后进行自增操作
LOG_CHECKPOINT_LSN 8 checkpoint的值
LOG_CHECKPOINT_OFFSET 4 checkpoint的值对应的在重做日志的偏移量

LOG_CHECKPOINT_LSN表示checkpoint的值,LSN小于该值的页都已经被写入到磁盘。CHECKPOINT BLOCK有两个,InnoDB会交替进行checkpoint值的更新。
这样即使某次checkpoint block写失败了,那么崩溃恢复的时候从上一次记录的checkpoint开始恢复,也能正确的恢复数据库事务。

恢复时,InnoDB需要读取这两个CHECKPOINT BLOCK,取其中较大的LOG_CHECKPOINT_LSN,恢复大于该值的重做日志。

LSN

LSN 即日志序列号( Log Sequence Number ),它是每个redo log的序号。

LSN存在多处,代表不同含义

1. 代表重做日志写入总量
LSN是单调递增的,保存在log_sys中(InnoDB运行时会维护一个对象,负责管理 Redo Log Buffer,启动时由log_init()函数负责初始化)
每写入一个redo log时,LSN就会递增该 Redo Log 写入的字节数,例如新增一个log长度是len,则log_sys->lsn += len。

2. 代表checkpoint最新位置,见上面的CHECKPOINT BLOCK中的LOG_CHECKPOINT_LSN。

当InnoDB正常shutdown,在flush redo log和脏页后,会做一次完全同步的checkpoint,并将checkpoint的LSN写到共享表空间的FSP HEADER PAGE的FIL_PAGE_FILE_FLUSH_LSN变量中。
(由于已经完全checkpoint,下次启动时,lsn可以被重新赋予常量初始值LOG_START_LSN)

Mysql启动时,会读取共享表空间中的FIL_PAGE_FILE_FLUSH_LSN,以及CHECKPOINT BLOCK中的较大的LOG_CHECKPOINT_LSN,如果两者相同,则说明正常关闭;否则,就需要进行故障恢复。
通过LOG_CHECKPOINT_LSN找到对应的redo log,扫描其后的redo log执行恢复操作即可。

3. 代表页最后刷新位置。每个页的头部都有一个FIL_PAGE_LSN,记录该页最后刷新时LSN的大小,可用于判断页是否需要进行恢复操作。

参数:innodb_fast_shutdown,控制数据库关闭操作
0:关闭时,需要完成所有full purge和merge insert buffer,并将所有脏页刷新到磁盘
1:默认值,只是将脏页刷新到磁盘
2:只保证日志都写入到日志文件,下次启动,会进行恢复操作

参数:innodb_force_recovery,控制数据库恢复操作
默认0,表示需要恢复时,进行所有的恢复操作。
其他配置值不一一列出。

undo

undo log保证事务的原子性, 帮助事务回滚以及MVCC功能。
undo是逻辑日志,对每行数据进行记录,记录的是每个操作的逆操作。
回滚操作,实际做的是先前相反的工作,对于insert,做一个delete,对于delete,做一个insert,对于update,做一个相反的update。

undo的存储

InnoDB将undo log看做数据,通过Page保存undo log。

回滚段
回滚段也是一个段对象,保存在页(0,6)处(共享表空间第6页),内容如下

变量 字节 描述
TRX_RSEG_MAX_SIZE 未使用
TRX_RSEG_HISTORY_SZIE 4 HISTORY链表中UNDO页的数量
TRX_RSGE_HISTORY 16 已提交的undo日志链表,可被purge回收
TRX_RSEG_FSEG_HEADER 10 回滚段的SEGMENT HEADER
TRX_RSEG_UNDO_SLOTS 4*1024 指向UNDO段SEGMENT HEADER所在页的偏移量

一个UNDO段可以管理一个事务,一个回滚段可以管理1024个UNDO段。
InnoDB1.1之前,只有一个回滚段,支持最大并发事务为1026。
InnoDB1.1开始,最大支持128个回滚段。
位于(0,5)的FIL_PAGE_TYPE_SYS,记录了所有回滚段所在页。

UNDO段
UNDO段是真正存储undo log的地方。它实际上是一个UNDO页链表。链表第一个UNDO页由以下部分组成:

  • UNDO LOG PAGE HEADER

  • UNDO LOG SEGMENT HEADER

  • UNDO日志

UNDO LOG PAGE HEADER内容如下

变量 字节 描述
TRX_UNDO_PAGE_TYPE 2 undo日志的类型,TRX_UNDO_INSERT或TRX_UNDO_UPDATE
TRX_UNOD_PAGE_STARE 2 UNDO页最新一个事务undo日志所在位置
TRX_UNDO_PAGE_FREE 2 UNDO页空闲的偏移量
TRX_UNDO_PAGE_NODE 12 UNDO页的链表节点

关于TRX_UNDO_PAGE_NODE,可以参考存储篇的链表结构

UNDO LOG SEGMENT HEADER内容如下

变量 字节 描述
TRX_UNDO_STATE 2 UNDO段的状态
TRX_UNDO_LAST_LOG 2 最近一个undo log header在页中的偏移量位置
TRX_UNDO_FSEG_HEADER 10 UNDO段的segment header
TRX_UNDO_PAGE_LIST 16 UNDO页的链表头

TRX_UNDO_STATE的有效值有TRX_UNDO_ACTIVE,TRX_UNDO_CACHEd,TRX_UNDO_TO_FREE,TRX_UNDO_TO_PURGE。

UNDO LOG SEGMENT HEADER仅保存在UNDO页链表的第一个UNDO页中,其他UNDO页中对应位置保留为空

undo记录结构
每个undo记录由两部分组成

  • UNDO LOG HEADER

  • UNDO LOG RECORD

undo log record有update undo log record和insert undo log record两种类型,通常insert操作产生insert undo log record,其他DML操作产生update undo log record。
通过TRX_UNDO_PAGE_TYPE可以看出,一个UNDO段只能存储一种类型的undo,insert undo log或update undo log。如果一个事务同时有INSERT,UPDATE操作,则需要每种类型分配单独的UNDO段,这样也会导致InnoDB支持最大并发事务数下降。

UNDO LOG HEADER内容如下

变量 字节 描述
TRX_UNDO_TRX_ID 8 产生undo日志的事务id
TRX_UNDO_TRX_NO 8 标识事务提交顺序的序号
TRX_UNDO_DEL_MARKS 2 标记本组 undo 日志中是否包含delete mark 产生的 undo 日志
TRX_UNDO_LOG_START 2 表示本组 undo 日志中第一条 undo 日志的在页面中的偏移量
TRX_UNDO_DICT_OPERATION 2 是否为DDL操作
TRX_UNDO_TABLE_ID 8 若是DDL操作,操作表的id
TRX_UNDO_NEXT_LOG 2 下一个UNDO LOG HEADER位置
TRX_UNDO_PREV_LOG 2 上一个UNDO LOG HEADER位置
TRX_UNDO_HISTORY_NODE 12 HISTORY链表节点

由于purge可能移除一些undo log record,TRX_UNDO_LOG_START不一定等于UNDO LOG HEADER结束位置偏移量。

事务开启时,会分配一个唯一的严格递增的事务ID以及UNDO段,并设置其TRX_UNDO_STATE变量为TRX_UNDO_ACTIVE。

注意:
InnoDB将undo log看做数据,UNDO页与普通的数据页一起管理,会依据LRU规则刷新出内存,后续再从磁盘读取。
同样,对undo log的操作也需要记录到redo log中。
如对于一个insert操作,redo log不仅要记录insert操作,还需要记录一个生成undo insert的操作。

进行恢复时,InnoDB会重做所有事务,包括未提交的事务和回滚了的事务。然后通过undo log回滚那些未提交的事务。

参数:
innodb_undo_directory:指定UNDO独立表空间位置
innodb_undo_logs:设置rollback segment个数,默认为128(一个rollback segment支持1024并发),在InnoDB 1.2,该参数替换之前版本的innodb_rollback_segments
innodb_undo_tablespaces:组成undo表空间文件个数
innodb_undo_log_truncate: MySQL 自动收缩 Undo 表空间,防止磁盘占用过大,默认开启(Mysql5.7.5之后提供)
innodb_max_undo_log_size:超过该阀值将被自动收缩

UNDO页复用
当事务提交时,需要处理UNDO页:

  • 如果当前的undo log只占一个page,且占用的header page大小使用不足其3/4时(TRX_UNDO_PAGE_REUSE_LIMIT),则状态设置为TRX_UNDO_CACHED,表示该UNDO页可以复用,之后新的undo log记录在当前undo log的后面。

  • 如果是Insert_undo(undo类型为TRX_UNDO_INSERT),则状态设置为TRX_UNDO_TO_FREE,该undo log可被删除

  • 如上不满足,则表明该undo log可能需要Purge线程去执行清理操作,状态设置为TRX_UNDO_TO_PURGE,将undo log加入到回滚段的TRX_RSGE_HISTORY中,由purge回收。

purge操作

purge用于最终完成delete和update操作,这样设计是因为InnoDB支持MVCC,所以记录不能在事务提交时立即进行处理,其他事务可能正在引用这行数据。
(delete操作将记录的delete flag设置为1)

前面说过,回滚段TRX_RSGE_HISTORY列表,会根据事务提交的顺序,将undo log链接起来。

执行purge过程中,InnoDB从TRX_RSGE_HISTORY列表中找到第一个需要被清理的记录trx1,清理后InnoDB会在trx1所在undo log页继续查找是否存在可以被清理的记录,直到该UNDO页没有可以清理的记录,再回到history list中查找下一个需要被清理的记录。
由于可以重用,一个undo log可能存放了不同事务的undo log。因此purge操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程。

MVCC原理

隐藏列
存储篇说过,行数据中有两个隐藏列用于实现MVCC
TransactionID:DB_TRX_ID,记录操作该数据事务的事务ID
RollPointer:DB_ROLL_PTR,指向上一个版本数据在undo log 里的位置指针

事务修改行数据时,会将修改前的数据放入undo log中,并修改TransactionID为当前事务ID,RollPointer指向上一个版本数据位置。
例如将行数据的一个字段从A -> B -> C,TransactionID,RollPointer变化如下

注意:这里通过RollPointer组织成一条undo log链。

快照
在RR级别下,事务在begin/start transaction之后的第一条select读操作后, 会创建一个快照(read view),将当前系统中活跃的其他事务记录记录起来。
在RC级别下,事务中每条select语句都会创建一个快照。

可见性判断
设要读取的行的最后提交事务id为 trx_id_current,
当前事务创建的快照read view 中最早的事务id为up_limit_id, 最迟的事务id为low_limit_id

1. trx_id_current < up_limit_id, 当前事务在读取该行记录时, 该行记录的最新事务ID是小于当前系统所有活跃的事务,所以当前行数据可见。

2. trx_id_current > low_limit_id, 当前事务开启后,该行记录被修改并提交,数据不可见。

3. up_limit_id <= trx_id_current <= low_limit_id,该行记录最新事务处于活动状态,
这时需要判断 trx_id_current 在不在 快照的活跃事务ID列表中。
若不在,数据可见。若在,不可见,需要查找undo log 链得到上一个版本再进行可见性判断。

group commit

对于InnoDB,事务提交进行两个操作:

  1. 修改内存中事务对应的信息,并将日志写入重做日志缓冲

  2. 调用fsync将重做日志缓冲写入磁盘
    group commit,组提交,即将多个事务的重做日志缓冲通过一次fsync刷新到磁盘

开启binlog后,为了保证存储引擎中的事务和binlog的一致性,InnoDB使用两阶段事务。
注意:重做日志是innodb产生,物理格式日志,记录对每个页的修改,在事务进行中不断写入。
而binlog是mysql上层产生的,是一种逻辑日志,在事务提交完成时一次写入。

两阶段事务步骤如下
Prepare 阶段:SQL 已经成功执行并生成 redo 和 undo 的内存日志;InnoDB 将回滚段设置为 prepare 状态;
binlog提交阶段:binlog 内存日志数据写入文件系统缓存并通过fsync() 写入磁盘;
Commit 阶段:fsync() 将 binlog 文件系统缓存日志数据永久写入磁盘;

恢复操作
在 prepare 阶段前崩溃,该事务直接回滚;
在 binlog 已经 fsync() ,但 InnoDB 未 commit 时崩溃;恢复时,将会从 binlog 中获取事务信息,重做该事务并提交,使 InnoDB 和 binlog 始终保持一致。

InnoDB需要保证 binlog 的写入顺序和 InnoDB 事务提交顺序一致。

我们使用on-line backup下来的备份文件进行恢复或者主备同步,因为InnoDB检测最新的事务T3已经Commit,不需要进行恢复,结果导致事务T1数据丢失。

InnoDB1.2版本前 ,使用 prepare_commit_mutex 保证顺序,只有当上一个事务 commit 后释放锁,下个事务才可以进行 prepare 操作。但导致开启二进制日志后,group commit功能失效,性能较差。

InnoDB1.2版本后进行了优化,
prepare 阶段不变,
binlog提交阶段和commit 阶段拆分为三个过程,每个阶段都会去维护一个队列,第一个进入该队列的作为 leader 线程,其他作为 follower 线程;leader 线程会收集 follower 的事务,并负责做 sync,follower 线程等待 leader 通知操作完成。

  • Flush阶段,将队列中每个事务的binlog都写入内存

  • Sync阶段,将内存队列中的binlog刷新到磁盘,若队列中有多个事务,仅一次fsync操作就完成日志的写入。
    Commit阶段,leader根据队列顺序调用InnoDB事务的提交,这时就可以使用group commit功能。
    由于三个阶段都是根据队列顺序执行操作,所以保证 binlog 的写入顺序和 InnoDB 事务提交顺序一致。

当有一组事物在进行Commit阶段时,其他新事物可以进行Flush阶段,从而使用group commit不断生效。

转载自:

深入理解InnoDB -- 事务篇

你可能感兴趣的:(深入理解InnoDB -- 事务篇)