回顾: InnoDB更新逻辑
了解了binlog之后,我们看InnoDB如何执行更新语句的。
- 执行器先找到ID=2这一行,由于ID是主键,引擎可以直接用树搜索到这一行。注意如果这一行所在的数据本来就在内存中,直接返回给执行器,否则需要从磁盘中读到内存里。
- 执行器拿到数据执行操作,得到一行新的数据,然后调用引擎接口写入这行新数据。
- 引擎将这行数据更新到内存中,同时将操作记录到redo log。此时redo log处于prepare阶段,告知执行器记录完了,随时可以提交
- 而后执行器生成这个操作的bin log,把bin log 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚写入redo log的记录改为commit提交阶段,更新完成
两阶段提交
我们注意到写入redo log 的步骤分为prepare和commit两个阶段。这就是两阶段提交,主要为了让两份日志保持逻辑一致
binlog会记录所有操作,并追加写入,系统定期做整库备份。假设我们找到的整库备份与需求记录点差一天,我们只需要从这个点开始根据binlog重做这一天所有的操作即可。
假设不使用两阶段提交,而先写某一个日志,写完了再写另一个
假设场景为,ID=2,这一行c=0;要将其加1,更新完第一个日志,之后再写第二个日志的时候发生了crash。
- 假设先写redo log ,在写binlog 时crash。由于redo log由crash-safe特性,仍可以恢复c被加了1,此时c=1.但是由于binlog没写完,如果用binlog恢复只会得到c=0。
- 假设先写bin log,后写redo log,由于还没有写redolog,会认定为这个事务无效,c还是=0。但是因为我已经写了binlog,如果用Bin log恢复就会多一个事务。
总之,如果不使用两阶段提交,就会导致用不同log恢复的状态不一样,即两份log逻辑无法保持一致。常见的做法是全量备份,再应用bin log
MySQL异常重启,怎么保证数据完整性
崩溃恢复规则:
- 如果redo log事务完整,(这个完整指已经有可commit标识),则直接提交
- 如果redo log事务只到prepare,则判断binlog,binlog完整直接提交,否则回滚
两种崩溃情形:
- 如果处于:写redo log处于prepare阶段,但在写binlog 之前,发生了崩溃:此时Binlog 还没写,即不会传到备库,redo log也没有提交,所以崩溃恢复很简单,直接回滚事务即可。
- 如果处于:binlog写完,但redo log还没commit就发生了崩溃。此时属于规则1,虽然没有commit但已经有commit标识,可以直接提交。
如何判断binlog是否完整
一个事务的binlog是有完整格式的:
- statement格式的binlog,最后会有COMMIT;
- row格式的binlog,最后会有一个XID event。
- binlog-checksum参数,可以用来验证binlog内容正确性。
崩溃时扫描顺序——redo log和binlog怎么关联起来的
二者有一个公共数据字段,叫XID.
崩溃恢复时,先按顺序扫描redo log
- 如果redo log既有prepare,又有commit标识,直接提交redolog
- 如果只有prepare,没有commit,则拿着XID去接着找binlog对应的事务。
为什么只需处于Prepare阶段的redo log和完整的binlog就可恢复
涉及到主备一致性问题 如果redolog prepare完,且binlog写完,发生了崩溃,此时binlog已经写入了,之后会被从库(或通过已经持久化的binlog恢复出来的库)拿出来恢复,此时主库和备库数据是一致的
两阶段提交的必要性
如上个问题,既然这样,为什么不 先写完redolog ,再写完binlog,要求二者都完整才可恢复?
事务的持久性问题
对于innodb来说,1.如果redo log提交完成了,那么事务就不能回滚(可能覆盖其他事务的更新)。
而2.如果redolog不等binlog直接提交了,之后写binlog的时候如果失败了,Innodb又必须要回滚,(但是redolog提交了不允许回滚了),此时数据 和binlog 就不一致了。
单用Binlog能不能支持crash-safe
不可。
- 历史原因:mysql原生的引擎并不是Binlog,接入mysql之后才发现binlog没有崩溃恢复的能力,所以拿出innnodb原有的redolog来弥补。
- 如果只用一个,同上2如果。连续的两个事务,第一个事务已经提交了,第二个事务再prepare之后,commit之前崩溃了,他必须要回滚,但是事务1已经提交了,我们认为其已经完成了,不会回滚事务1.InnoDB引擎使用的是WAL技术,执行事务的时候,写完内存和日志,事务就算完成了。如果之后崩溃,要依赖于日志来恢复数据页。也就是说在这个位置发生崩溃的话,事务1也是可能丢失了的,而且是数据页级的丢失。此时,binlog里面并没有记录数据页的更新细节,是补不回来的
单用redolog能不能支持crash-safe
如果只从崩溃恢复角度,可以。这样就没有两阶段提交了。
但是binlog有redo log无法替代的功能:
- 归档。redolog是循环写,写到末尾就要回到开头继续写,这样日志无法保留,redolog起不到归档作用
- Mysql高可用的基础,是binlog的复制
- 有些系统依赖于Binlog来做下游系统的输入。
先修改内存还是先写redo log。redolog buffer
比如一个事务,插入了两条记录。
插入记录的过程中,就生成日志,但这些日志不能在redolog还没commit就写到redolog的文件里
redo log buffer就是一块内存,保存上述redo的日志,即执行第一个插入的时候,先修改了redo log buffer的内存数据(写入上述日志)
真正写redo log 文件得执行commit语句才写入redolog 文件
如何保证redo log /bin log 完整
binlog 写入机制
事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。
系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
- 图中的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设置成一个大值,可以提高性能。比较常见的是将其设置为100~1000中的某个数值。
但是,风险是,如果主机重启,会丢失最近n个事务的binlog
redo log 写入机制
事务在执行过程中,生成的redo log是要先写到redo log buffer的。
但redo log buffer并不是每次生成之后就持久化到磁盘
如果事务执行期间MySQL发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。
redo log可能存在的三种状态:
- 存在redo log buffer中,物理上是在MySQL进程内存中,就是图中的红色部分;
- 写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面,也就是图中的黄色部分;
- 持久化到磁盘,对应的是hard disk,也就是图中的绿色部分。
日志写到redo log buffer是很快的,wirte到page cache也差不多,但是持久化到磁盘的速度就慢多了。
为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,它有三种可能取值:
- 设置为0的时候,表示每次事务提交时都只是把redo log留在redo log buffer中;
- 设置为1的时候,表示每次事务提交时都将redo log直接持久化到磁盘;
- 设置为2的时候,表示每次事务提交时都只是把redo log写到page cache。
以下三种场景会让一个没有提交的事务的redolog写入到磁盘中。
- InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。事务执行中间过程的redo log也是直接写在redo log buffer中的,这些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在redolog buffer里的日志一起持久化到磁盘。
时序上,redo log先pre,再写bin log,最后把redo log commit
双1配置:
是fync_binlog和innodb_flush_log_at_trx_commit都设置成 1。即每次提交,redo log 和bin log都要持久化到硬盘
如果把innodb_flush_log_at_trx_commit设置成1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是要依赖于prepare 的redo log,再加上binlog来恢复的。
每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了。
也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog。
组提交机制
LSN:绍日志逻辑序列号(log sequence number,LSN)LSN是单调递增的,用来对应redo log的一个个写入点。每次写入长度为length的redo log, LSN的值就会加上length。
在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。
实际上写Bin log 分为两步:
1.先从bin cache 写到binlog
2.再fsync持久化
为了让组提交的效果更好,把redo log做fsync的时间拖到了步骤1之后。也就是说,上面的图变成了这样:
这么一来,binlog也可以组提交了。在执行图5中第4步把binlog fsync到磁盘时,如果有多个事务的binlog已经写完了,也是一起持久化的,这样也可以减少IOPS的消耗。不过通常情况下第3步执行得会很快,所以binlog的write和fsync间的间隔时间短,导致能集合到一起持久化的binlog比较少,因此binlog的组提交的效果通常不如redo log的效果那么好。
如果你想提升binlog组提交的效果,可以通过设置 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。
常见问题
WAL机制为什么快
所以WAL机制主要得益于:
- redo log 和 binlog都是顺序写,磁盘的顺序写比随机写速度要快;
- 组提交机制,可以大幅度降低磁盘的IOPS消耗。|
IO瓶颈
针对这个问题,可以考虑以下三种方法:
- 设置 binlog_group_commit_sync_delay和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
- 将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。
- 将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。