事务的提交
1. 概述
要了解事务的提交过程,首先需要对mysql的基本逻辑结构以及一条sql语句的执行过程先有个大概;
1.1 mysql 逻辑结构
1.2 更新语句的执行过程
以简单的更新语句可以知道大概的执行流程如下:
从上面我们就可以得出,一个事务是需要以下几个过程的:
服务层:
- 连接器:客户端连接服务器,鉴权等等
- 分析器:语法分析
- 优化器:选择索引
最后 执行器 操作 inno db
引擎层执行下列操作
- 先从数据页内存中查询相关的数据
- 如果内存中不存在,那么从磁盘里面查找,并且加载到内存中
- 将旧数据记录到
undo log
- 修改对应的数据并且更新到内存
- 事务提交步骤1:将数据页的更新记录写入到
redo log buffer
,且设置为prepare
阶段 - 事务提交步骤2:将逻辑日志写入到
bin log
- 事务提交步骤3:
redo log
设置为commit
状态
所以一个事务是需要数据引擎层以及服务层相互配合执行;主要涉及到的是以下三个日志:
- 服务层的就是
bin log
; inno db
引擎层的undo log
inno db
引擎层的redo log
1.3 WAL
WAL,全称是Write-Ahead Logging, 预写日志系统。指的是 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上(日志主要分为 undo log、redo log、binlog);具体来说就是如果要更新一条数据的时候如果直接去更新,那么会涉及到很多的随机磁盘IO,这是个很费性能的操作,而是先在内存中修改相应的数据,然后以日志的形式,顺序写入日志文件,最后再将内存中的数据,通过后台脚本慢慢的刷到磁盘里面。
2. 日志 redo log
redo log (重做日志)
: innodb 引擎层的日志,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做(后面会有介绍)。
InnoDB 的 redo log
是固定大小的,可以通过参数 innodb_log_files_in_group
配置,默认是2个,每个文件大小由innodb_log_file_size
决定;从ib_logfile0
开始,ib_logfile{innodb_log_files_in_group-1}
结束;路径是在mysql的安装路径下面的data
目录下:
2.1 逻辑结构
比如可以配置为一组 4 个文件,每个是1个GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos
是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
checkpoint
是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos
和 checkpoint
之间还空着的部分,可以用来记录新的操作。如果 write pos
追上 checkpoint
,表示日志满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint
推进一下。
2.2 写入机制
下面是redo log 的内存分布图
从上面可以看出redo log
可能存在三种状态,分别是:
- 存在
redo log buffer
中,物理上是在 MySQL 进程内存中,就是图中的红色部分; - 写到磁盘 (
write
),但是没有持久化(fsync
),物理上是在文件系统的 page cache 里面,也就是图中的黄色部分; - 持久化到磁盘(
fsync
),对应的是 hard disk,也就是图中的绿色部分。
日志写到 redo log buffer 是很快的,wirte 到 page cache 也差不多,但是持久化到磁盘的速度就慢多了。为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit
参数,它有三种可能取值:
- 设置为 0 的时候,表示每次事务提交时都只是把
redo log
留在redo log buffer
中 ;MySQL 崩溃就会丢失。 - 设置为 1 的时候,表示每次事务提交时都将
redo log
直接持久化到磁盘;断电也不会丢失。 - 设置为 2 的时候,表示每次事务提交时都只是把
redo log
写到page cache
。MySQL 崩溃不会丢失,断电会丢失。
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer
中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。注意,事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些 redo log 也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。
实际上,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redo log 写入到磁盘中:
- redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
- 并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。
假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit
设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务 A 在 redo log buffer 里的日志一起持久化到磁盘。
这里需要说明的是,我们介绍两阶段提交的时候说过,时序上 redo log 先 prepare, 再写 binlog,最后再把 redo log commit。如果把 innodb_flush_log_at_trx_commit 设置成 1,那么 redo log 在 prepare 阶段就要持久化一次,因为有一个崩溃恢复逻辑是要依赖于 prepare 的 redo log,再加上 binlog 来恢复的。
每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB 就认为 redo log 在 commit 的时候就不需要 fsync 了,只会 write 到文件系统的 page cache 中就够了。
通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。
3. 日志 undo log
undo log
回滚日志,也是inno db 存储引擎层日志;这里简单来讲就是记录 记录被修改前数据,方便后续事务回滚。当前还有其他重要作用(MVCC)。这里就简单的过一下。
4. 日志 bin log
bin log (归档日志)
:mysql 服务层日志,记录的是 记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ;因为它其实相当于归档所以主要用于主从复制,数据恢复。同样默认的存放位置也是在安装目录下面的data
目录,
4.1 客户端相关的命令
可以直接通过msyql 客户端执行一些命令来查看binlog
show binary logs
; – 获取binlog文件列表show master status
; – 查看当前正在写入的binlog文件show binlog events in "binlog.000034"
; – 查看指定binlog文件的内容show binlog events
; – 只查看第一个binlog文件的内容
4.2 数据格式
4.2.1 Row(5.7默认)
记录操作语句对具体行的操作以及操作前的整行信息。缺点是占空间大。优点是能保证数据安全,不会发生遗漏。
内容可以通过mysql自带的工具mysqlbinlog
进行解析查看;命令:” mysqlbinlog
+ bin log 文件名” 来查看,一个事务的结尾会有 “ Xid” 标记(作为三步提交时判断事务是否执行完成的判断标记),内容格式如下:
比如如下数据表, 执行语句 update test set age=6 where id=23
通过命令mysqlbinlog --no-defaults -vv {binlog 文件路径}
可以得出以下数据:
1)server id 1,表示这个事务是在 server_id=1 的这个库上执行的。
2)每个 event 都有 CRC32 的值,可以通过参数 binlog_checksum 设置。
3)Table_map event 跟在图 5 中看到的相同,显示了接下来要打开的表,map 到数字 75。现在我们这条 SQL 语句只操作了一张表,如果要操作多张表呢?每个表都有一个对应的 Table_map event、都会 map 到一个单独的数字,用于区分对不同表的操作。
4)我们在 mysqlbinlog 的命令中,使用了 -vv 参数是为了把内容都解析出来,所以从结果里面可以看到各个字段的值(比如,@1=23、 @2=‘xx’ 这些值,个人猜测是为了节约内存,直接用数字下标标识字段)。
5)最后的 Xid event,用于表示事务被正确地提交了。
4.2.2 Statement
记录修改的 sql。缺点是在 mysql 集群时可能会导致操作不一致从而使得数据不一致(比如在操作中加入了Now()函数,主从数据库操作的时间不同结果也不同)。优点是占空间小,执行快。
4.2.3 Mixed
会针对于操作的 sql 选择使用Row 还是 Statement。相比于 row 更省空间,但还是可能发生主从不一致的情况。
4.3 写入机制
和 redo log 类似,binlog 写到磁盘上的过程也分为三种状态:binlog cache(每个线程各有一份)、page cache、disk。
写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size
用于控制单个线程内 binlog cache
所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。事务提交的时候,执行器把 binlog cache
里的完整事务写入到 bin log
中,并清空 binlog cache
。状态如下所示。
从上面写盘状态可以看到,每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
图中的 write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
图中的 fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IOPS。
write 和 fsync 的时机,是由参数 sync_binlog 控制的:
- sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
- sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
- sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。
5. redo log 与 bin log 联系
5.1 crash-safe
最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。什么是crash-safe
在InnoDB 存储引擎中,事务提交过程中任何阶段,MySQL突然奔溃,重启后都能保证事务的完整性,已提交的数据不会丢失,未提交完整的数据会自动进行回滚。这个能力依赖上面的2个日志:redo log
和undo log
。
5.2 异同点
redo log
与 bin log
这两种日志有以下三点不同:
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
两者都经历三种状态:
MySQL 的 Cache、Page cache、磁盘。只不过 redo log 在 MySQL 的 Cache 是全局共用的,而 binlog 在 MySQL 中的 Cache 是线程私有的,每个线程都有一份。同时两者的 write 操作写入 Page Cache 都非常快(因为在内存中),而 fsync 到磁盘都比较慢(因为需要进行磁盘IO)。
6. 事务的提交分析
6.1 事务的2阶段提交(2PC)
从上面来看,当我们执行事务的 commit
语句时,对应的也就是会依次执行以下步骤:
- 事务提交步骤1:将数据页的更新记录写入到
redo log buffer
,且设置为prepare
阶段。 - 事务提交步骤2:将逻辑日志写入到
bin log。
- 事务提交步骤3:
redo log
设置为commit
状态。
将 redo log
的写入拆成了两个步骤:prepare 和 commit,这就是”事务的两阶段提交“。
为什么必须有“两阶段提交”呢?
这是为了让两份日志(bin log以及redo log
)之间的逻辑一致。
如何恢复
举例如果在图中时刻 A 的地方,也就是写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。
恢复事务的规则如下:
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
- 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:
- a. 如果是,则提交事务;
- b. 否则,回滚事务。
所以如果在时刻 B,也就是 binlog 写完,redo log 还没 commit 前发生 crash,那么就符合上面的第二条的a规则。
其他:
2PC这种思路其实也是分布式事务的解决方案之一,由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交」
整个过程有2个角色参与,一个就是事务管理器 还有一个就是参与者
当你要执行一个事务,这个事务操作的数据库是在不同的服务器上的,那么现在就需要一个中间件作为协调者来控制这整个事务流程的执行,简单来说就是分3个阶段:
- 中间件告诉需要操作的数据库节点,通知参与者作准备,让它们开启XA事务,然后判断所有数据库节点是否已经处于prepare状态。
- 中间件记录下日志,中间件判断事务提交还是回滚的阶段。如果所有节点都prepare那就统一提交。但凡出现一个失败的节点,统一回滚。
- 参与者进行提交或者回滚。
所在站在inno db的事务提交就可以看出,bin log 就是那个中间件用于协调 “子系统” ,inno db redo log 作为参与者 。
6.2 双 “1”设置
通常为了crash-safe ,会将 innodb_flush_log_at_trx_commit
这个参数设置成 1 ,表示redo log 在 prepare 阶段就要持久化一次到磁盘。同样sync_binlog 这个参数也设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘;这样可以保证 MySQL 异常重启之后数据不丢失。但是这又会带来新的问题:一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog;那么按照这样的推导,当数据库TPS是3w的时候,也就是说需要6w次的磁盘IO,这是完全会影响性能的;所以在mysql 5.6
引入了bin log 与redo log 都可以 组提交。
6.3 组提交
是MYSQL处理日志的一种优化方式,主要为了解决写日志时频繁刷磁盘的问题。
最开始Redo log的刷盘操作将会是最终影响MySQL TPS的瓶颈所在。为了缓解这一问题,MySQL使用了组提交,将多个刷盘操作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。
为了保证Redo log和binlog的数据一致性,MySQL使用了二阶段提交,由binlog作为事务的协调者。而 引入二阶段提交 使得binlog又成为了性能瓶颈,先前的Redo log 组提交 也成了摆设。为了再次缓解这一问题,5.6官方版本 MySQL增加了binlog的组提交,目的同样是将binlog的多个刷盘操作合并成一个,结合Redo log本身已经实现的 组提交,分为三个阶段(Flush 阶段、Sync 阶段、Commit 阶段)完成binlog 组提交,最大化每次刷盘的收益,弱化磁盘瓶颈,提高性能。
日志逻辑序列号(log sequence number,LSN):
LSN 是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN 的值就会加上 length。LSN 也会写到 InnoDB 的数据页中,来确保数据页不会被多次执行重复的 redo log;
6.3.1 基本思路
注意从上面可以得到:当设置为“双 1” 时,我们知道 提交一个事务的适合 redo log 会在 prepare 阶段 刷一次盘;
数据库肯定是支持并发事务的,所以“组提交”其实就是想办法让尽可能的收集多的 需要刷盘的 日志 一起刷盘。也可以当作一个队列, 先到得事务自然就是”头”也就是“leader”,然后这个leader就开始等待看看后面还有没有事务过来, 如果等待了一定时间 或者 在一定时间内后面又来了一定数量的事务 这些事务就可以通称为一个“组” 以组为单位一起调用fsync 进行刷盘 。
那么相应得上面的 “等待一定时间” 和“一定数量事务”有相关的配置:binlog_group_commit_sync_delay
和 binlog_group_commit_sync_no_delay_count
来实现。
binlog_group_commit_sync_delay
参数,表示延迟多少微秒后才调用 fsync;
binlog_group_commit_sync_no_delay_count
参数,表示累积多少次以后才调用 fsync。
这两个条件是或的关系,也就是说只要有一个满足条件就会调用 fsync。所以,当 binlog_group_commit_sync_delay
设置为 0 的时候,binlog_group_commit_sync_no_delay_count
也无效了。
以下面为例,三个并发事务 (trx1, trx2, trx3) 在 prepare 阶段,都写完 redo log buffer,持久化到磁盘的过程,对应的 LSN 分别是 50、120 和 160。
- trx1 是第一个到达的,会被选为这组的 leader;
- 等 trx1 要开始写盘的时候,这个组里面已经有了三个事务,这时候 LSN 也变成了 160;
- trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的 redo log,都已经被持久化到磁盘;
- 这时候 trx2 和 trx3 就可以直接返回了。
所以,一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。在并发更新场景下,第一个事务写完 redo log buffer 以后,接下来这个 fsync 越晚调用,组员可能越多,节约 IOPS 的效果就越好。
6.3.2 组提交–2阶段提交
之前得提交过程如下:
MySQL 为了让组提交的效果更好,把 redo log 做 fsync 的时间拖到了步骤 1 之后。也就是说,上面的图变成了这样:
这么一来,binlog 也可以组提交了。在执行图 5 中第 4 步把 binlog fsync 到磁盘时,如果有多个事务的 binlog 已经写完了,也是一起持久化的,这样也可以减少 IOPS 的消耗。不过通常情况下第 3 步执行得会很快,所以 binlog 的 write 和 fsync 间的间隔时间短,导致能集合到一起持久化的 binlog 比较少,因此 binlog 的组提交的效果通常不如 redo log 的效果那么好。
7 最后
对于事务的提交过程一直比较模糊,特别是redo log
与undo log
之间的关联关系;近期又重新去翻了下 MySQL 实战 45 讲,算是其中的读后感,所以文章中很多内容也是来源于此;稍微作了下整理。