一、WAL机制:
1、意义:
只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复。
2、WAL机制主要得益于两个方面:
①、redo log 和 binlog都是顺序写,磁盘的顺序写比随机写速度要快;
②、组提交机制,可以大幅度降低磁盘的IOPS消耗。
二、binlog的写入机制:
1、写入过程:
事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了binlog cache的保存问题。系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache。
由图可以看出:
①、每个线程有自己binlog cache,但是共用同一份binlog文件。
②、图中的write,指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。
③、图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS。
2、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日志。
三、redo log的写入机制:
1、写入过程:
事务在执行过程中,生成的redo log先写到redo log buffer。
2、redo log buffer里面的内容,是不是每次生成后都要直接持久化到磁盘?
不是。如果事务执行期间MySQL发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。
3、事务还没提交的时候,redo log buffer中的部分日志有没有可能被持久化到磁盘呢?
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。
InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。
注意:事务执行中间过程的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也是可能已经持久化到磁盘的。
4、让一个没有提交的事务的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里的日志一起持久化到磁盘。
四、MySQL的“双1”配置:
1、概念:
指的是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog。
2、组提交(group commit)机制:
<1>、日志逻辑序列号(log sequence number,LSN):
LSN是单调递增的,用来对应redo log的一个个写入点。每次写入长度为length的redo log, LSN的值就会加上length。LSN也会写到InnoDB的数据页中,来确保数据页不会被多次执行重复的redo log。
<2>、组提交的过程:
如图所示,是三个并发事务(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的效果就越好。为了让一次fsync带的组员更多,MySQL会优化——拖时间。也就是在两阶段提交的时候,先把binlog从binlog cache中写到磁盘上的binlog文件;再调用fsync持久化。
<3>、提升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。
这两个条件是或的关系,也就是说只要有一个满足条件就会调用fsync。所以当binlog_group_commit_sync_delay设置为0时,binlog_group_commit_sync_no_delay_count就无效了。
五、如果MySQL现在出现了性能瓶颈且瓶颈在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。这样做的风险是,主机掉电的时候会丢数据。
注意:不建议把innodb_flush_log_at_trx_commit 设置成0。因为把这个参数设置成0,表示redo log只保存在内存中,这样的话MySQL本身异常重启也会丢数据,风险太大。而redo log写到文件系统的page cache的速度也是很快的,所以将这个参数设置成2跟设置成0其实性能差不多,但这样做MySQL异常重启时就不会丢数据了,相比之下风险会更小。
六、相关问题:
问题一:执行一个update语句以后,再去执行hexdump命令直接查看ibd文件内容,为什么没有看到数据有改变呢?
回答:这可能是因为WAL机制的原因。update语句执行完成后,InnoDB只保证写完了redo log、内存,可能还没来得及将数据写到磁盘。
问题2:为什么binlog cache是每个线程自己维护的,而redo log buffer是全局共用的?
回答:MySQL这么设计的主要原因是,binlog是不能“被打断的”。一个事务的binlog必须连续写,因此要整个事务完成后,再一起写到文件里。而redo log并没有这个要求,中间有生成的日志可以写到redo log buffer中。redo log buffer中的内容还能“搭便车”,其他事务提交的时候可以被一起写到磁盘中。
问题3:事务执行期间,还没到提交阶段,如果发生crash的话,redo log肯定丢了,这会不会导致主备不一致呢?
回答:不会。因为这时候binlog 也还在binlog cache里,没发给备库。crash以后redo log和binlog都没有了,从业务角度看这个事务也没有提交,所以数据是一致的。
问题4:如果binlog写完盘以后发生crash,这时候还没给客户端答复就重启了。等客户端再重连进来,发现事务已经提交成功了,这是不是bug?
回答:不是。实际上数据库的crash-safe保证的是:
①、如果客户端收到事务成功的消息,事务就一定持久化了;
②、如果客户端收到事务失败(比如主键冲突、回滚等)的消息,事务就一定失败了;
③、如果客户端收到“执行异常”的消息,应用需要重连后通过查询当前状态来继续后续的逻辑。此时数据库只需要保证内部(数据和日志之间,主库和备库之间)一致就可以了。