Files
rikako-note/mysql/mysql文档/mysql_事务.md
2025-08-28 20:09:19 +08:00

674 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 事务
## 事务分类
事务通常可分为如下类型:
- 扁平事务(flat transaction)
- 带保存点的扁平事务(flat transaction with savepoints)
- 链事务(chained transaction)
- 嵌套事务(nested transaction)
- 分布式事务(distributed transaction)
### 扁平事务
扁平事务为最常用的事务,在扁平事务中,所有操作都处于统一层次,其由`begin work`开始,并由`commit work``rollback work`结束。
扁平事务的操作是原子的,要么都提交,要么都回滚。
### 带有保存点的扁平事务
对于带有保存点的扁平事务,`其支持在事务执行过程中回滚到同一事务中较早的一个状态`
在事务执行过程中,可能并不希望所有的操作都回滚,放弃所有操作的代价可能太大。通过`保存点`,可以记住事务当前的状态,在后续发生错误后,事务能够回到保存点当时的状态。
相较于扁平事务只能够全部回滚,带保存点的扁平事务能够回滚到保存点时的状态。
保存点可以通过`save work`来创建,使用示例如下所示
<img alt="" height="752" src="https://i-blog.csdnimg.cn/blog_migrate/2ef380492373bb8e4a6e529109baee3c.png" width="728">
### 链事务
可视为保存点事务的一个变种。在使用带保存点的事务时,如果系统发生崩溃,那么所有的保存点都会消失。`在后续重启进行恢复时,事务需要从开始处重新执行`,而不是从最近的一个保存点开始执行。
> 若事务在数据库未提交时发生崩溃那么在数据库再次重启执行recovery操作时会对未提交的事务进行回滚即使之前事务存在保存点也会全部回滚
链事务的思想是,当提交事务时,释放不必要的数据对象,将必要的上下文隐式传递给下一个要开始的事务。`提交事务和开始下一个事务操作必须为原子操作`,下一个事务必须徐要能看到上一个事务的结果,示例如下:
<img class="trans" src="https://images2015.cnblogs.com/blog/754297/201602/754297-20160204112125600-267403241.jpg">
和带保存点的扁平事务不同的是,带保存点的扁平事务能够回滚到任意正确的保存点,而链事务只能回滚当前事务。
且链事务和带保存点的扁平事务,对于锁的处理也不同:
- `链事务`对于每个事务commit后释放持有的锁
- `带保存点的扁平事务`:在整个事务提交前,不会释放持有的锁
### 嵌套事务
嵌套事务为一个层次结构框架,由顶层事务控制各个层次的事务。嵌套在顶层事务中的事务被称为`子事务`
<img src="https://pic2.zhimg.com/v2-01f00b04df29181143da399008b92055_r.jpg">
如下为Moss理论嵌套事务的定义
- 嵌套事务是由若干事务组成的一颗树,子树既可以是嵌套事务,又可以是扁平事务
- 处在叶子节点的事务是扁平事务
- 位于根节点的事务被称为顶层事务,其他事务被称为子事务,事务的`predecessor`被称为父事务,事务的下一层事务被称为子事务
- 子事务既可以提交又可以回滚,但是子事务的提交并不会立马生效,除非其父事务已经被提交。`任何子事务都在顶层事务提交后才真正提交`
- 树中任何一个事务的回滚会引其所有子事务都一起回滚
在Moss理论中实际工作被交由叶子节点来完成`只有叶子节点的事务才能够访问数据库,发送消息,获取其他类型的资源``高层事务仅仅负责逻辑控制,即负责何时调用相关子事务`
即使一个系统不支持嵌套事务,也可以通过保存点技术来模拟嵌套事务。
#### savepoint和嵌套事务区别
在使用保存点来模拟嵌套事务时,在锁持有方面和嵌套事务有差别。
- 嵌套事务:在使用嵌套事务时,不同子事务在数据库持有的锁不同
- 保存点:在通过保存点来模拟嵌套事务时,用户无法选择哪些锁被哪些子事务继承,无论有多少个保存点,所有的锁都可以得到访问
### 分布式事务
通常是在分布式环境运行的扁平事务,需要访问网络中的不同节点。
分布式事务同样需要满足ACID的特性要么都发生要么都不发生。
对于innodb存储引擎其支持扁平事务、带有保存点的扁平事务、链事务、分布式事务。`innodb并不原生支持嵌套事务单可以通过带保存点的事务来模拟串行的嵌套事务`
## 事务实现
对于事务的ACID特性其实现如下
- `I(隔离性)`:事务隔离性通过锁来实现
- `A(原子性), D(持久性)`事务的原子性和持久性可以通过redo log来实现
- `C(一致性)`:事务一致性通过undo log来实现
### redo
redo log重做日志用于实现事务的持久性其由两部分组成
- 内存中的重做日志缓冲redo log buffer
- 磁盘中的重做日志文件redo log file
#### redo log persists before transaction committed
innodb存储引擎支持事务其通过`force log at commit`机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到磁盘的日志文件中进行持久化,直到该过程完成后事务才提交完成。
> 上述描述中`提交时将事务所有日志写入到磁盘的日志文件中`,这句话中`日志`代表`redo log 和 undo log`。
> - redo log用于保证事务持久性
> - undo log用于帮助事务回滚也用于mvcc功能
#### redo log和undo log比较
- redo log在mysql server运行时只会被顺序的写入server运行时并不会被读取
- undo log则不同在server运行时可能需要对事务进行回滚或执行mvcc操作此时可能需要对undo log文件进行`随机读写`
#### redo log和binlog的比较
binglog通常用于对数据库进行`point in time`形式的恢复从某个时间点起恢复数据以及主从复制但是binlog和redo log的差别如下
- binlog针对的是mysql数据库级别不止用于innodb还用于其他存储引擎
- redo log属于innodb存储引擎级别
- binlog实际记录的是对应sql语句属于逻辑日志
- redo log实际记录的格式则是物理格式具体为针对某个页面的物理修改
redo log和 bin log日志写入磁盘的时机也有所不同
- bin log`仅在事务提交后才进行一次写入`
- redo log`在事务执行过程中也可能发生写入`redo log buffer满后会写入到磁盘中故而redo log file中同一事务写入的redo log内容可能并非是连续的`多个事务在写入redo log时可能会交叉写入`
#### innodb_flush_log_at_trx_commit
参数`innodb_flush_log_at_trx_commit`用于控制redo log/ undo log刷新到磁盘的策略该参数默认值为`1`,即事务提交时刷新日志到磁盘。除了默认值之外,还可以为该参数设置如下值:
- 0 事务提交时不刷新日志到磁盘仅在master thread中刷新日志到磁盘master thread中刷新操作每秒触发一次
- 2事务提交时刷新日志但是仅将日志写入到文件系统的缓存中`并不进行fsync操作`
`innodb_flush_log_at_trx_commit`参数设置为`0``2`虽然可以在一定幅度上提高性能但是会丧失数据库的ACID特性。
#### log block
在innodb中redo都是以512字节的大小为单位进行存储的即redo log buffer、redo log file都是以block的形式进行保存block的大小为512字节。
##### block & atomic
若针对相同的页redo log的大小大于512字节那么其会被分割为多个block进行存储。且由于redo log block的大小和磁盘扇区相同故而在将block时无需使用double write机制针对特定的block起写入为原子的要么写入成功要么写入失败不会像页page一样存在dirty的情况。
redo log block中包含的内容除了日志本身外还包含`log block header``log block tailer`内容。`log block header + log block content + log block tailer`合计占用512字节其中各部分大小如下
- `log block header`: 12字节大小
- `log block content`: 492字节大小
- `log block tailer`: 8字节大小
如上所示每个redo log block可实际存储的内容大小为492字节。
##### log block header
log block header大小为12字节由如下部分组成
- `LOG_BLOCK_HDR_NO`: 占用4字节
- `LOG_BLOCK_HDR_DATA_LEN`: 占用2字节
- `LOG_BLOCK_FIRST_REC_GROUP`: 占用2字节
- `LOG_BLOCK_CHECKPOINT_NO`: 占用4字节
###### `LOG_BLOCK_HDR_NO`
log buffer由log block所组成可以将log buffer看作是log block的数组故而log block header中`LOG_BLOCK_HDR_NO`起代表当前block在buffer中的位置。
`LOG_BLOCK_HDR_NO`由于表示的是log buffer中的数组小标故而可知`LOG_BLOCK_HDR_NO`其是递增的,并且可以循环使用。`LOG_BLOCK_HDR_NO`的大小为4字节但是其首位用作`flush bit`,故而,其可表示的最大长度为`2^31 bytes = 2GiB`
##### `LOG_BLOCK_HDR_DATA_LEN`
`LOG_BLOCK_HDR_DATA_LEN`大小为2字节代表`log block`所占用的大小当log block被写满时该值为`0x200`表示当前log block使用完`block`中所有的可用空间即log block的大小为512字节。
##### `LOG_BLOCK_FIRST_REC_GROUP`
`LOG_BLOCK_FIRST_REC_GROUP`占用2个字节表示log block中第一个日志所处的偏移量。
`LOG_BLOCK-FIRST_REC_GROUP`的取值可能存在如下场景:
- 该值大小和`LOG_BLOCK_HDR_DATA_LEN`相同则代表当前block中`不包含新的日志`即当前block中存储的存储的全是上一block中record的后续部分
下图表示`事务T1的重做日志占用762字节``事务T2的重做日志占用100字节`的场景。
<img alt="" class="has" src="https://i-blog.csdnimg.cn/blog_migrate/41c0ed462afe1e9d56e1e53f11175f9a.jpeg">
由于每个block中最多只能保存492字节的数据故而T1事务的762字节需要分布在两个block中第一个block保存492字节的数据第二个block中保存剩余270字节的数据。
- 其中左侧的block`LOG_BLOCK_FIRST_REC_GROUP`值为12代表第一个record开始的位置紧接在log block header之后
- 而右侧的block`LOG_BLOCK_FIRST_REC_GROUP`的值为`12 + 270 = 282 bytes`。在存放第一条record之前不仅有log block header对应的12字节还有之前T1剩余日志的270字节
##### `LOG_BLOCK_CHECKPOINT_NO`
`LOG_BLOCK_CHECKPOINT_NO`占用4字节大小代表log block最后被写入时的检查点第四字节的值。
LSNlog sequence number为一个`全局唯一且单调递增的64位数字当发生数据修改时redo log内容会增加此时LSN也会增加`
CHECKPOINT则是一个LSN值同样为64位整数代表位于`CHECKPOINT`之前所有的修改已经被持久化到数据库中,`位于CHECKPOINT之前的redo log内容可以被安全的覆盖`
##### log block tailer
log block tailer中仅由一个部分组成`LOG_BLOCK_TRL_NO`,其值和`LOG_BLOCK_HDR_NO`相同。
#### log group
log group被称为重做日志组其中包含多个redo log文件innodb中只有一个log group。
log group只是一个逻辑上的概念由多个redo log file组成。log group中每个redo log file大小相同。redo log file中存储的是redo log block`在innodb引擎运行过程中会将redo log buffer中的log block刷新到磁盘文件中。`
##### redo log buffer刷新到磁盘中的时机
redo log buffer会在如下时机将log block刷新到磁盘中
- 事务提交时
- log buffer中有一半的内存空间已经被使用时
- log checkpoint时(checkpoint时会导致脏页被刷新到磁盘上而WAL要求脏页刷新前刷新redo log buffer)
##### WAL
`write-ahead logging`是一种为database系统提供`原子性``持久性`的技术。
`write ahead log``append-only`的辅助磁盘存储结构用于crash recovery和transaction recovery。
在使用`WAL`的系统中在所有的changes被应用到数据库之前要求changes都被写入到log中。
所以在innodb中脏页被刷新到磁盘之前脏页对应的`newest_lsn`之前的redo log都必须被刷新到磁盘中。`redo log file中最新的lsn必须大于磁盘页文件中最大的lsn`
在redo log buffer中的log block刷新到redo log file中时其会追加append到redo log file的末尾。当redo log group中的一个文件被写满时其会接着写入下一个redo log file其行为称为`round-robin`
在redo log group中的每个redo log file中其前2KB4个log block大小均不用于存储log block前2KB内容如下
- 对于log group中的第一个redo log file前2KB用于存储如下内容下列每个部分大小均为一个block
- log file header
- checkpoint 1
-
- checkpoint2
- 对于log group中`非第一个redo log file`其仅保留开头2KB的空间但并不保存信息
#### redo log format
innodb中存储管理是基于页的故而redo log format格式也基于页。
##### redo log头部格式
redo log头部格式通常包含3部分
- `redo_log_type` 重做日志类型
- space 表空间id
- page_no 页的偏移量
之后redo log body部分根据重做日志类型的不同会存储不同内容。
#### LSN
LSN代表的是日志序列号其大小为8字节且单调递增。`LSN代表事务写入redo log的总字节数`
##### 页LSN
在每个页的头部,都存在`FIL_PAGE_LSN`其记录了该页的lsn。在页中`LSN`代表该页最后刷新时的lsn大小。
FIL_PAGE_LSN在`buffer pool page``disk page`中均存在,二者记录的值不同:
- `buffer pool page` header中`FIL_PAGE_LSN`记录的是`内存页最后被修改的LSN`
- `disk page` header中`FIL_PAGE_LSN`记录的是最后被刷新到磁盘的页对应的最大修改LSN
在执行crash recovery过程中会从CHECKPOINT开始一直到redo log file末尾逐条处理redo log record对于每条redo log record关联的页会比较`record_lsn``FIL_PAGE_LSN`的大小:
- `record_lsn <= FIL_PAGE_LSN`代表当前redo record对应的修改已经包含在页中当前redo log record直接跳过即可
- `record_lsn > FIL_PAGE_LSN`代表当前redo record中的修改不存在于页中需要对页应用record修改并在修改完后更新页的`FIL_PAGE_LSN`
##### 查看LSN
可以通过`show engine innodb status`来查看LSN情况核心数值如下
- `log sequence number`代表当前LSN
- `log flushed up to` 代表已经刷新到磁盘文件中的LSN
- `last checkpoint at`: 代表页已经刷新到磁盘的LSN
#### recovery
innodb在启动时`不管上次数据库运行是否正常关闭,都会尝试执行恢复`
`redo log是物理日志故而恢复速度相较逻辑日志要快得多`,恢复操作仅需从`checkpoint`开始。
例如,对于如下数据表
```sql
create table t (
a int,
b int,
primary key(a),
key(b)
);
```
若执行sql语句
```sql
insert into t select 1,2;
```
在执行时,需要修改如下内容:
- 聚簇索引页(包含数据)
- 辅助索引页
故而,其记录重做日志内容大致为
```
page(2,3), offset 32, value 1,2; # 聚簇索引页
page(2,4), offset 64, value 2; # 辅助索引页
```
由上述示例可知redo log为物理日志记录的是对页的物理修改故而`redo log是幂等的`
### undo
redo log记录了对页的物理操作可以用于进行`redo`。而undo和redo不同undo主要用于对事务的回滚。
undo的存放位置和redo不同
- redo log存放在redo log file中
- undo log存放在数据库内部的segment中该segment被称为`undo segment``undo段位于共享的表空间内`
#### undo log和redo log差异
- redo log为物理日志记录的是对页的修改而undo log则是逻辑日志对每个insert操作undo log会生成一个相反的delete对update也会生成另一个逆向的update
- redo log是全局的innodb中所有事务都会`向同一个redo log交叉写入`而undo log则是针对事务的每个事务都有其自己的undo log chain
#### 非锁定读
除了用于事务回滚外undo log还可以用于MVCC。当事务A尝试读取一条记录R时如果记录R已经被另一个事务B占用那么事务A可以通过undo log读取行数据之前的版本。
上述实现被称为`非锁定读`
#### undo log的产生会伴随redo log的产生
`undo log其本质仍然是数据`。undo log其存放在表空间的undo segment中仍然可被可做是数据`WAL(write-ahead logging)要求变更被应用到数据库之前,需要先写入日志`
故而在生成undo log时对于undo页的修改也会被记录到redo log中。
#### 存储管理
innodb通过segment来管理undo log其管理方式如下
- innodb包含rollback segment
- 每个rollback segment会记录1024个undo log segment
- undo log segment中会进行undo页的申请
- 共享表空间偏移量为5的页会记录所有rollback segment header所在的页
- 偏移量为5的页类型为FIL_PAGE_TYPE_SYS
##### innodb_undo_directory
该参数用于设置rollback segment文件所在的路径默认为`./`,代表`datadir`
如果`innodb_undo_directory`变量没有被定义那么undo tablespace将会被创建再`datadir`下。默认情况下undo tablespaces文件的名称为`undo_001``undo_002`
##### innodb_rollback_segments
每个undo tablespace支持最大128个rollback segments`innodb_rollback_segments`变量定义了rolback segments的数量。
每个rollback segments支持的事务数量由`rollback segment中undo slot的数量``每个事务需要的undo log数量`来决定。
> 当innodb页大小为16KB时rollback segment中undo slot的数量为`innodb page size/ 16`即1024个。
##### innodb_undo_tablespaces
该变量设置了undo tablespaces的数量。
##### purge
在事务提交之后并不能立刻删除undo log以及undo log所在的页`其他事务仍有可能通过undo log来还原数据行的之前版本`。故而在事务提交时会将undo log放入到一个链表中交由purge线程来决定是否最终删除undo log以及undo log所在的页。
> purge代表`清空不再被需要的旧版本数据行及其对应的undo log记录。
如果为每一个事务分配一个单独的undo页那么会非常浪费存储空间。由于事务提交时所分配的undo页并不能立刻释放故而当数据库负载较大时可能同时存在大量的undo页会占用相当多的存储空间。
##### undo页的重用设计
在innodb对undo页的设计中考虑了对undo页的重用。当事务提交时首先会将undo log放在链表中然后判断undo页的使用空间是否小于`3/4`。如果是代表该undo页可以被重用之后新的undo log会记录在当前undo log的后面。
> 即undo page是可被重用的当事务提交时如果undo log的使用空间小于3/4那么该undo页是可以被重用的一个undo页中可能包含多个undo log
#### 核心概念
##### rollback segment
undo tablespace由rollback segment构成每个undo tablespace最多支持128个rollback segment`innodb_rollback_segments`定义了rollback segments的数量。
##### undo slots
undo slot是rollback segment内的slot由rollback segment进行管理。
undo slot主要用于关联undo segment`当事务启动时系统会从rollback segment中获取一个空闲的undo slot``成功获取undo slot后即代表关联了一个undo segment`
每个undo log slot中会存储一个`page_no`其指向undo log segment的起始页位置。
##### undo segment
undo segment为undo slot指向的空间undo segment中包含多个undo pages而undo segment中则包含了undo log。
#### undo log格式
在innodb存储引擎中undo log分为如下两种类型
- insert undo log:
- insert undo log是事务在insert操作中产生的undo log
- 在事务提交之前事务插入的数据对其他事务不可见而事务提交之后事务插入的数据对其他读已提交的事务才可见故而insert undo log在事务提交之后不再被需要因为在读已提交隔离级别下insert undo log是可见的在可重复读的隔离级别下insert undo log则是不可见的没有中间版本只能可见/不可见
- 故而当事务提交之后即可删除该事务关联的insert undo log
- update undo log
##### insert undo log
在事务提交之后insert undo log即可被删除故而无需purge操作。
insert undo log结构如下
- next下一个undo log的位置长度为2字节
- type_cmpl: 记录undo的类型对于insert undo来说该值为11。该字段长度为1字节
- undo_no记录事务的id压缩后保存
- table_id: 记录undo log对应的表对象压缩后保存
- 记录所有主键的列和值(本次插入的数据,可能多条)
- start位于undo log尾部记录undo log的开始位置长度为2字节
> ##### rollback
> 在执行rollback操作时可以根据insert undo log中存储的table id主键列、主键值来定位需要回滚的行数据直接删除回滚数据即可。
<img src="https://ts3.tc.mm.bing.net/th/id/OIP-C.DUFx18elzLQKwUNUss-FLgAAAA?rs=1&amp;pid=ImgDetMain&amp;o=7&amp;rm=3" alt="mysql redo log 事务大_MySQL事务实现及Redo Log和Undo Log详解-CSDN博客" class=" nofocus" tabindex="0" aria-label="mysql redo log 事务大_MySQL事务实现及Redo Log和Undo Log详解-CSDN博客" role="button">
##### update undo log
update undo log针对的是`delete``update`操作。在mvcc机制的实现中需要用到该undo log故而`update undo log在事务提交后不能立刻删除`
update undo log的格式如下图所示。
<img src="https://img-blog.csdnimg.cn/20190917162925836.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NhcmFmaW5hNTI3,size_16,color_FFFFFF,t_70" alt="MySQL的redo log、undo log、binlog_mysql redolog undolog binlog-CSDN博客" class=" nofocus" tabindex="0" aria-label="MySQL的redo log、undo log、binlog_mysql redolog undolog binlog-CSDN博客" role="button">
对于update undo log其哥字段含义如下
- type_cmpl: 对update undo log`type_cmpl`可能的取值如下:
- 12TRX_UNDO_UPD_EXIST_REC更新未被标记为delete的记录
- 13TRX_UNDO_UPD_DEL_REC更新`已经被标记为delete的记录`
- 14TRX_UNDO_DEL_MARK_REC将记录标记为delete
- update vector: update vector中记录了update操作导致发生改变的列每个被修改的列信息都记录在undo log中。
> 在执行delete操作时并不会直接对行数据进行物理删除操作而是`将行数据标记为delete`待后续purge操作中才会实际对数据进行物理删除。
undo log中主要存储旧的col值用于在回滚或mvcc时为undo操作提供信息还原数据先前的版本。
在实际生成insert/update undo log时对于插入、更新、删除等操作其实际生成undo log的方式如下
- insert操作
- 对于insert操作其实际生成一条insert undo log`type_cmpl`的值为`TRX_UNDO_INSERT_REC`
- delete操作
- 对于delete操作其实际生成一条`type_cmpl`值为`TRX_UNDO_DEL_MARK_REC`的undo log
- delete操作并不会直接对记录进行物理删除而是将记录标记为delete后续进行purge操作时才会实际删除
- update记录的非主键值
- 在对记录的非主键值进行update时会生成一条`type_cmpl`值为`TRX_UNDO_UPD_EXIST_REC`的记录,用于对更新操作进行回滚
- update记录的主键值
- 当对记录主键列进行修改时会生成两条undo log
- `TRX_UNDO_DEL_MARK_REC`类型的记录,对应`将原记录标记为删除`的回滚
- `TRX_UNDO_INSERT_REC`类型的记录,标记对`新纪录`的插入回滚
> 对于insert/update undo log record每条undo record均只针对一条记录。故而当一条sql语句对多行记录进行删除/更改时会生成多条insert/update undo record。
### undo log组织形式
上述内容介绍了undo log record的格式为了对undo log records进行高效率的访问和管理undo log record应当以适当的形式被组织。
#### undo log的逻辑组织方式
每个事务都会修改一系列的records并产生相应的undo log records事务对应的undo log由这些undo log records组成。
undo log除了由undo log record组成外其还包含了一个undo log header结构如下
<img src="https://yqintl.alicdn.com/4eeedc0d845c26417e570782cb01824f98d7dcd3.png" alt="3" title="3">
如上述结构所示undo log header中包含如下部分
- trx id: 事务id用于标识产生该undo log的事务
- trx no: trx no用于表示事务的提交顺序header中trx no并非一定有值在事务提交前该field未定义当事务提交后该field会被填充
- `trx no`用于判断`是否purge应当被执行`
- delete mark: 判断是否undo log中存在`TRX_UNDO_DEL_MARK_REC`类型的undo record从而在purge时避免扫描
- log start offset: 该field记录了undo log header结尾的位置便于后续对undo log header进行拓展
- History List Nodeundo log通过该结构挂载到History List中
> 故而undo log就粒度存在两种结构
> - undo record: 记录每行数据的历史版本行数据通过维护undo record chain来还原历史版本
> - undo log: 针对事务级别每个undo log由undo log header和多个undo records组成并且undo log会根据事务的提交顺序被挂载到history list中
##### record versions
如果多个事务针对同一条record进行了修改串行那么每个事务对record造成的修改都会生成不同的版本version不同的版本之间通过链表进行链接用于后续的mvcc示例如下
<img src="https://yqintl.alicdn.com/ee4f6e18f7fb39e128e06d26daed3432ff203390.png" alt="4" title="4">
如上所示,事务`I, J, K`都对`id=1`的记录进行了操作,其操作顺序如下:
1. 事务I插入了id为1的记录并且将`a`的值设置为A
2. 事务J将id为1记录的`a`修改为了B
3. 事务K将id为1记录的`a`修改为了C
事务I, J, K都有其事务对应的undo log并且每个undo log都拥有对应的undo log header和undo log records。
通过index中的记录及其rollptr引用的链表可以对记录关联的`I, J, K`三个版本进行还原。
#### 物理组织方式
undo log的结构如上述所示我们无法控制事务生成undo log的大小当事务操作大量的数据时其undo log对应的undo records数量也会变多undo log大小更大。但是`最终生成undo log写入磁盘时会基于固定的大小进行写入16KB`
由于事务关联`undo log`的大小是无法控制的,其大小可能需要多个页来进行存储。故而,`对占用空间较大的undo logundo page会按照需要进行分配``而对于大小较小的undo log会将多个undo log放置在同一个undo page中`
> undo log和undo page的对应关系是灵活的既可能一个undo log占用多个undo pages也可能多个undo log共用相同的undo page
关于undo log的物理组织方式如下图所示
<img src="https://yqintl.alicdn.com/4680cd1e8572e5f39310973603c43445a49ee236.png" alt="5" title="5">
##### undo segment
每当一个事务开启时都需要持有一个undo segment对于undo segment中磁盘空间的释放和占用对16KB页的释放和占用都由FSP segment进行管理。
undo segment中会至少持有一个undo page并且`每个undo page都会记录undo page header`
##### undo page header
undo page header中包含如下内容
- undo page typeundo page的类型
- last log record offset最后一条记录的offset
- free space offsetpage中空闲空间的offset
- undo page list node指向List中的下一个undo page
undo segment中的第一个undo page除了undo page header外`还会记录undo segment header`
##### undo segment header
undo segment header中包含如下内容
- state该field记录了udno segment的状态TRX_UNDO_CACHED/TRX_UNDO_PURGE
- undo segment中最后一条undo record的位置
- 当前segment被分配的undo page组成的链表
##### undo log storing
undo page中的空间用于存储undo log对于`大小较小的undo log`innodb会对undo page进行reuse在undo page中存储多个undo logs避免浪费空间。而对于大小较大的undo log会使用多个undo page来对该undo log进行存储。undo page reuse只会发生在segment的第一个page
#### 文件组织方式
在同一时刻一个undo segment只属于一个事务一个undo segment无法同时被多个事务共享。每当事务开启时都会获取一个undo segment故而在多个事务并行的运行时需要多个undo segment。
##### rollback segment
在innodb中每个rollback segment中包含了1024个undo slot而每个undo tablespace最多包含128个rollback segment。
rollback segment header中包含了128个slot每个slot包含4字节并且都指向`某个undo segment的第一页`
undo slot的数量会影响innodb数据库中的事务并行程度
#### 内存组织方式
innodb中针对undo log的内存数据奇结构如下
<img src="https://yqintl.alicdn.com/70a8768288fddc5a8e5b9c0845e004e374392211.png" alt="7" title="7" data-spm-anchor-id="a2c65.11461447.0.i1.319b654a1qaiie">
##### undo::Tablespace
对于磁盘中的每个undo tablespace都会在内存中维护一个`undo::Tablespace`结构,`undo::Tablespace`结构中,最重要的部分是`trx_rseg_t`
##### trx_rseg_t
`trx_rseg_t`关联了rollback segment header。除了基础元数据之外其还包含了四个`trx_undo_t`类型的链表:
- Update List: update list中包含`用于记录update undo record的undo segments`
- Update Cachedupdate cached list中包含可以被重用的update undo segments
- Insert Listinsert list中包含了正在使用的insert undo segments
- Insert Cachedisnert cached list中包含了后续可以被重用的insert undo segments
##### trx_undo_t
trx_undo_t关联的则是上面描述的undo segment。
#### undo writing
当一个写事务开启时,将会通过`trx_assign_rseg_durable`分配Rollback Segment内存中的trx_t也会指向对应的trx_rseg_t内存结构。
rollback segment的分配策略很简单会依次尝试下一个活跃的rollback segment。在此之后如果事务内的第一条修改命令需要写undo record将会调用`trx_undo_assign_undo`来获取undo segment。在获取undo segment时`trx_rseg_t`中包含的cached list中节点将会被优先使用。
> 如果已经存在`已分配但是未被使用的undo segment`将会优先使用这部分undo segment。
>
> 如果不存在已分配但是未被使用的undo segment将会调用trx_undo_create创建新的undo segment。
##### undo record写入
当undo page被写满后会调用`trx_undo_add_page`来向当前undo segment中添加新的undo page。在将undo record写入到undo page时存在如下约束
- 一条undo record无法跨页面存在于两个page如果当前page剩下的空间不足以写入undo record那么会将undo record写入到下一个undo page
在事务完成之后(提交/回滚),
- `如果当前undo segment中只使用了一个undo page并且undo page的使用率小于75%`那么该undo segment将会被保留并添加到对应的insert/update cachedlist中
- `如果undo segment中undo page大于一个或者undo page的使用率大于75%`如么对于undo segment的处理如下
- 对于insert类型的undo segment其会在事务提交/回滚时直接被回收
- 对于update类型的undo segment其会在purge完成后被回收
根据场景的不同undo segment其header的状态将会从`TRX_UNDO_ACTIVE`变为`TRX_UNDO_FREE`/`TRX_UNDO_TO_PURGE`/`TRX_UNDO_CACHED`其代表了innodb事务的结束。
#### undo for rollback
在回滚时会按照undo record生成的时序逆向进行回滚。回滚根据undo record的内容。
#### undo for mvcc
mvcc中`“多版本”设计是为了读事务和写事务之间互相等待`。在传统的读写锁设计中,读写/写写之间都是互斥的,只要有事务持有读锁,则其他事务拿不到写锁;如果有事务持有写锁,则其他事务拿不到读锁和写锁。在多事务同时进行并发操作的场景下,如果采用读写锁进行相互等待,那么将会对性能造成很大的影响。
在采用多版本的设计后每个读事务在对record进行读取时`无需对record添加锁而是查看读事务可见的历史版本即可`
##### 历史版本
在多版本设计中,“历史版本”在设计上等效于如下描述:
- 在读事务开启时,会针对整个数据库中的生成一个快照,后续该事务的读操作都从该快照中获取数据
### purge
`t`中,`a`为聚簇索引,`b`为辅助索引若执行如下sql
```sql
delete from t where a=1;
```
上图示例中undo segment中包含了2个page和3个undo record
- 第一个undo page中包含了undo record 1
- 第二个undo page中包含了undo record 2和undo record 3
回滚流程如下所示:
- 从undo segment header中获取last undo page header的位置
- 从last undo page header中获取最后一条undo record的位置即undo record 3的位置并根据undo record 3的内容执行回滚操作
- 根据prev record offset的值来获取上一条undo record的位置如此逆向遍历undo page中所有的undo record并且执行回滚操作
- 在当前page中如果所有的undo records都执行完回滚操作会根据undo page header查询前一个undo page并按照上述流程执行回滚操作
上述语句造成的修改如下:
- 将主键`a`为1的记录标记为delete将记录`delete flag`设置为1聚簇索引中的数据并没有被实际物理删除`且会生成针对聚簇索引的undo log`。当事务发生回滚时,仅需将`delete flag`重新设置为0即可完成对删除的回滚。
- 对于辅助索引中满足`a=1`的记录,同样不会做任何处理,`也不会产生针对辅助索引的undo log`
> undo log只针对聚簇索引生成对于辅助索引的变化并不会生成对应的undo log。
>
> 从上文的undo log内容来看update undo log和insert undo log都记录了唯一索引聚簇索引的值并且update undo log记录了各修改列的oldValue二者均不涉及辅助索引内容。
可知在delete语句执行时并不会马上就对记录进行物理删除而是将记录标记为delete记录实际的删除在purge操作时才被执行。
对于记录的`delete`操作和`update`操作update主键列时会先将原记录标记为删除后插入一条新的记录会在purge操作中完成。purge确保了innodb存储引擎对于MVCC机制的支持记录不能再事务提交时立刻进行处理仍会有其他事务会访问该记录的旧版本。
是否可以对记录进行物理删除由purge来决定若某行记录不被其他任何事务引用可以执行物理delete操作。
#### history
innodb存储引擎中维护了一个history列表`根据事务的提交顺序`对undo log进行链接`在history list中先提交的事务其undo log位于history list的尾端`
<img src="https://img-blog.csdnimg.cn/8bf680dd74014ed593d9f51a06448667.png" alt="Mysql undo log_mysql undolog-CSDN博客" class=" nofocus" tabindex="0" aria-label="Mysql undo log_mysql undolog-CSDN博客" role="button" data-bm="6">
##### history的purge流程
已知先提交的事务位于history list的尾端故而purge操作会从尾端开始查找需要被清理的记录。以上图为例purge操作执行流程如下
- 从尾端找到第一个需要被清理的记录此处为trx1
- 清理完trx1之后其会在trx 1的undo log为位于的undo page中继续查找是否可清空其他记录此处可以找到`trx3``trx5被其他事务引用不能清理
- 再次去history list中查找找到trx2清理trx2
- 找到trx2 undo log所在的undo页然后清理trx 6和trx 4
上述流程中purge会首先从history list中查找undo log然后会随便清除undo log位于的undo page中其他可以被清理的undo log`这样能够减少page的随机访问次数提高性能`
##### innodb_purge_batch_size
`innodb_purge_batch_size`用于设置每次purge操作需要清理的undo page数量该值默认为`300`
通常该值设置越大每次回收的undo page越多可供重用的undo page也越多会减少新开undo page的开销。但是该值设置过大时会增加purge占用的cpu和io资源令用户线程的资源减少。
##### innodb_max_purge_lag
当innodb存储引擎压力较大时并无法高效进行purge操作。此时history list长度会越来越长。
参数`innodb_max_purge_lag`用于控制history list的长度当history list长度大于`innodb_max_purge_lag`的值时(`innodb_max_purge_lag`值需大于0会对用户的DML操作进行延缓延缓算法如下
```
delay = ((length(history_list) - innodb_max_purge_lag) * 10) - 5
```
其中delay的值为ms且delay针对的是每一行。例如当一个update操作需要更新5行数据时每行数据都要被delay故而故而该update操作的总delay为`5 * delay`
delay会在每一次purge操作完成后重新计算。
##### innodb_max_purge_lag_delay
该参数用于控制delay的最大值。当基于公式计算出的delay大于`innodb_max_purge_lag_delay`delay的值取`innodb_max_purge_lag_delay`避免delay值过大导致用户线程无限等待。
### group commit
#### binlog
在mysql中binlog通常用于replication和point-in-time recovery等方面。在binlog中每个修改数据库内容的事件都会被记录。
binlog是一系列log files的集合其中记录了mysql的数据修改和结构变化binlog确保了所有事务都能够在slave servers之间准确的被复制维护了在分布式环境下的一致性。如果发生宕机可以通过binlog将数据库寰宇拿到其已知的`最后良好状态`
##### replication && point-in-time recovery
`replication`涉及到从mysql master server到salve server复制数据的过程。通过binlogslave server能够按照事件发生的顺序从master server中复制数据。
`point-in-time recovery`则是支持将数据库恢复到先前的某一个时间点通过binlog的replay即可实现。
##### binlog format
mysql支持三种binlog类型
- row其会记录row的变更提供受影响数据的详细细节
- statement其会记录`造成修改的实际sql语句`。其记录的信息不如row详细但是会更加紧凑
- mixed其整合了row和statement格式会基于不同事务的需要动态在`row``statement`之间进行切换
#### group commit
group commit通常用于提高事务处理系统的性能。group commit将多个事务绑定在一起系统能够减少磁盘写入次数并且条性能特别是在事务负载较高的系统中。
##### group commit机制
group commit允许多个事务合并为一次提交而不是每个事务单独提交。该方案会减少磁盘的io操作提高整体吞吐量。在高并发场景下相比于每个事务单独提交group commit机制能够显著提高性能。
##### `binlog_order_commits`
`binlog_order_commits`为global variables并且无需停止server的运行就能对该变量进行设置。
如果该变量的值为`off(0)`,则代表事务可以被并行提交。在部分场景下,其能够带来性能的提升。
##### `binlog_max_flush_queue_time = microseconds`
##### binlog details
当server在执行事务时会收集事务造成的修改并将其存放到`per-connection``transaction cache`中。如果使用了statement-based replication那么statement将会被写入到transaction cache中如果使用了row-based replication那么row changes将会被写入到transaction cache中。一旦事务提交transaction cache将会作为一个single block被写入到binary log中。
上述的binlog写入流程能够令每个session都独立的执行每个connection都有其独立的transaction cache并且仅需在写入transaction data到binary log时加锁即可。由于事务之间是相互隔离的故而在提交时对事务进行序列化即可。
##### sync the storage engine and binary log
binlog机制mysql上层的和存储引擎无关而redo log机制是和存储引擎绑定的。为了对存储引擎和binlog进行同步server使用了2PC协议tow-phase commit protocol
<img src="https://img-blog.csdn.net/20161222103535710?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
如上图所示使用2pc提交协议是为了能够保证`要么事务同时位于engine和binlog中要么事务都不位于engine和binlog中`。故而,不应该出现`事务位于binlog却不位于engine中或事务出现在engine中却不出现在binlog中`的场景即使server在prepare后发生崩溃并后续恢复。
通过两阶段提交可以实现上述需要求一旦事务在engine中prepare其可以被完全提交或完全回滚即engine和binlog全部提交或全部回滚即使prepare后server发生崩溃并后续恢复。在recovery阶段存储引擎能够提供所有prepared但是尚未提交的事务并且之后
- 如果事务可以在binlog被找到那么事务将会被提交
- 如果事务在binlog中找不到那么事务将会被回滚
> 如果`innodb_flush_log_at_trx_commit`被设置为1那么在2pc的prepare阶段就被持久化到磁盘中后续再对binlog进行write和fsync。故而当执行crash recovery时如果redo log为prepared状态且对应事务在binlog中存在那么事务在redo log中一定存在可以直接对redo log进行commit但如果事务在binlog中不存在那么可以对innodb事务进行回滚。
在大多场景下如果binlog不存在那么仅需对所有prepared transaction进行回滚即可。
> 当使用`on-line backup`方法(例如`Innodb Hot Backup`这些工具会直接拷贝当前数据库的文件和innodb的transaction logs。但是transaction logs也有可能包含prepared状态的事务。在通过备份恢复时也会对所有prepared事务及逆行回滚令数据库处于一致的状态。
>
> 并且,`on-line backup`经常被用于启动新slave。`last committed transaction`在binary log中的log position被记录到了redo log header中在recovery时recovery program将会打印binary log中last committed transaction的位置。
##### prepare_commit_mutex
为了保证`on-line backup`行为的正确性事务进行提交的顺序必须要和其被写入到binlog中的顺序一致如果不能保证顺序的一致性将会出现如下问题
- 如果事务按照`T1, T2, T3`的顺序写入到binlog但是其提交顺序为`T1, T3, T2`那么将会导致根据binlog执行恢复时binlog中间出现空洞例如T1和T3已经被提交但是T2尚未被提交导致binlog中T1和T3的binlog block都是commit但是T2却是prepared
<img src="https://img-blog.csdn.net/20161222103606133?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
<img src="https://img-blog.csdn.net/20161222103703259?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
> 上图中,事务提交顺序为`T2, T3`但是binlog中的顺序为`T1, T2, T3`。从而导致位于binlog中最靠前位置的T1还未提交为prepared状态但是T2和T3的binlog处于commit状态从而binlog发生空洞
在slave机器上发生recovery时如果binlog存在空洞slave将会直接跳过空洞从而导致slave中事务缺失。
为了解决该问题innodb补充了`prepare_commit_mutex`当对事务进行prepare时该mutex将会被获取并且在事务commit时被释放。故而在多事务场景下一个事务`prepare-write-flush-commit`流程中不会插入其他操作binlog写入事务的顺序和innodb存储引擎中事务提交的顺序是一致的。
> `prepare_commit_mutex`会将执行`prepare-write-commit`操作的事务串行化由于将binlog写入到文件比较耗时故而引入prepare_commit_mutext会造成bin log写入较慢。
##### binary log group commit
为了在`保证事务写binlog顺序和innodb存储引擎提交顺序一致`的基础上提高性能,故而不应采用`prepare_commit_mutex`,而是采用了如下设计。
`binary log group commit`实现将commit过程拆分为了如下图所示的多个阶段
<img src="https://img-blog.csdn.net/20161222103803619?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hhb2NoZW5zaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
上述stages位于binlog commit过程的内部并且不会造成其他影响。由于commit过程被拆分为了如下stages故而可以同时存在数个线程同时对事务进行处理这样会增加吞吐量。
对于每个stage都存在一个queuesession在queue中排队等待被处理。如果一个session被注册到了空的queue那么其将被认为是stage leader如果session被注册时queue不为空那么其将作为stage follower。stage leader将会将queue中所有的threads经过stage处理并且将其注册到下一个stage并且将leader和所有的followers注册到下个stage。
followers会被移动并且等待leader通知整个commit都执行完成。
leader可能会注册到一个非空queue中即leader可以决定成为一个follower但是follower永远不会变成leader。
当leader进入到stage后会一次性将queue抓取并依次处理queue中排队的session。当queue被抓取后当leader在处理old queue时其余的session又可以被注册到stage。
- `Flush Stage` 在flush stage所有注册到queue的session都会将其cache写入到binlog中。`实际将内容刷新到系统缓冲区中但并不实际调用fsyncfsync的调用发生在sync stage`
- `sync stage`在sync stage会根据`sync_binlog`的设置将binary log同步到磁盘中。如果`sync_binlog`值为1所有被flushed的session都会被同步到磁盘中
- `commit stage`在commit stage所有session将会按照其register的顺序在engine中进行提交该步骤由stage leader完成。由于在commit procedure中每个stage都保留了顺序那么binlog中写入事务的顺序和引擎中事务的提交顺序是一致的。
当commit stage执行完成后commit stage queue中所有的线程都将被标记为完成并且会向所有线程都发送signal令其继续执行。
由于leader注册到下一个stage时可能变为follower最慢的stage可能会积累最多的工作。通常情况下sync stage会积累最多的工作。但是向flush阶段填充尽可能多的事务是至关重要的flush stage会被单独处理。
在flush stageleader将会逐个扫描flush queue中的sessions扫描过程在满足下列任一条件时终止
- 当queue为空时leader将会立即进入下一阶段并且将所有sessions注册到sync stage中
- 如果`从第一个任务被unqueue起`已经超过时长限制`binlog_max_flush_queue_time`那么整个queue都会被抓取并且sessions的transaction cache都会被flushed处理流程和未超时场景下一致。之后leader再回进入到下一阶段。
> 通过将sessions在stage queue进行排队在write、sync、commit多个阶段都能进行group处理。这样能够提高多事务场景下的事务提交性能通过binary log group commit也能大大减少fsync的磁盘写入次数。