23-MySQL是怎么保证数据不丢的?
# binlog 的写入机制
事务执行过程中,先把日志写入到 binlog cache 中,事务提交后再把 binlog cache 写入到 binlog。无论这个事务有多大都要保证一次性写入。
binlog cache 是系统给分配的内存空间,每个线程一个。binlog_cache_size 是控制每个线程分配的 binlog cache 大小。如果超过这个数值就要暂存到磁盘。但是中间有个过程。
首先 binlog cache 会将数据 write 到 page cache 中。
笔记
page cache 是操作系统用于缓存磁盘数据的一种机制。当应用程序需要读取磁盘上的数据时,操作系统会先查看这些数据是否已经在 page cache 中。如果在,就直接从内存中读取,这比从磁盘读取要快得多,同样,当应用程序需要写入数据到磁盘时,操作系统通常会先将数据写入到 page cache,然后在适当的时候再将数据从 page cache 写入到磁盘。
然后在适当的时机 fsync 到磁盘,这个过程是要消耗 IO 的。
这个 write 和 fsync 的时机判定是由参数 sync_binlog 来控制的。
- sync_binlog = 0:代表每次提交的事务只 write,而不 fsync。
- sync_binlog = 1:代表每次提交的事务都会 fsync。
- sync_binlog = N:write N 次后进行一次 fsync。
一般来说这个值设置在 100-1000 之间,不要设置成 0,一旦设置成 0,写入磁盘的时机就完全依赖于系统了,这种虽然能够很大限度上提高 IO 但是一旦崩溃,就会造成事务丢失。设置成 N 和这个道理也是一样的。
# redolog 的写入机制
redo log 的日志是要先写入到 redo log buffer 中的,而 redo log buffer 并不是每一次生成后都写入到磁盘中。
也不怕丢失,因为事务没有提交,即使丢失了也无所谓。
那反过来说却有些不同,如果事务没有提交,也会有一部分数据被持久化到磁盘中。
redo log 也有三种状态
- 存在 redo log buffer 中。
- write,但没有持久化,也就是写到 page cache 里。
- 持久化到磁盘中。
innodb_flush_log_at_trx_commit 参数
- 为 0 的时候每次事务提交都存在 redolog buffer 中。
- 为 1 的时候每次事务提交都将 redolog 持久化到磁盘。
- 为 2 的时候表是每次提交都把 redolog 写入到 page cache。
说回没提交的事务被写入到磁盘的问题,innodb 后台有一个线程,每隔一秒会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。所有的 redolog 都是写入在 redolog buffer 中的,所以会出现这种情况。
还有两种方式也会让没提交的事务被写入到磁盘
- redo log buffer 占用即将达到 innodb_log_buffer_size 的一半的时候,后台线程会主动写盘(到 page cache)。
- 并行任务提交的时候,顺带将这个事务的 redolog buffer 持久化到磁盘。比如线程 A 在事务里写入了一些数据到 redolog buffer,innodb_flush_log_at_trx_commit 设置的值为 1,这个时候线程 B 正好提交了事务,那么就会立刻将 redolog buffer 中的数据写入到磁盘,会连带着 A 的没提交的数据一起。
由于两阶段提交,redolog 会先 prepare,然后在写 binlog,在最后把 redo log commit 了。
但如果把 innodb_flush_log_at_trx_commit 设置为 1 ,那么 redo log 在 prepare 阶段就会持久化,因为崩溃恢复要依赖于这个逻辑。
如果说崩溃恢复后看到 redo log 中有 commit 的标识就说明已经提交了不用管。
如果只有完整的 prepare,就需要根据 binlog 的日志来进行判断,如果 binlog 中有完整的事务信息,那么就会根据 redo log 回滚所有未完成的事务,并使用 binlog 来执行已提交但未完成的事务了。
就是根据这个特性,innodb 认为可以不用每次提交就写盘,只用写入 page cache 就能完成了。
双 1 策略就是 ync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1,即一个完整事务提交之前需要等待两次刷盘,一次是 redo log prepare 阶段,一次是 binlog 落盘。
如果 MySQL 的 TPS 能够跑到 2 万的话岂不是每秒就有 4 万次写磁盘?
并不是,Mysql 会用到组提交的机制。
首先要明确的是 LSN(日志逻辑序号),LSN 是递增的,在每个事务提交的是时候才会生成,保证事务的顺序性。
比如多个并发事务都 prepare 了,第一个提交的事务 LSN 为 50,当第一个事务要去 redo log fsync 的时候这个组里已经有三个事务了,LSN 也变成了 150 了,那么事务 1 去写盘的时候带的 LSN 就是 150 了,等于 150 之前的事务都会被持久化到磁盘。
所以如果 fsync 越晚,写的次数就越少。
并且 Mysql 这里也用到了一个优化,之前说过,整个流程是
但是这里的写 binlog 也是个动作,而真正的是
binlog 先 write 到 page cache。
然后再 fsync。
这里做的优化就是 redolog 的 fsync 会推到到 binlog write 之后,以等待更多的事务写入,然后将积累的数据一起 fsync。
binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 可以来修改 binlog 组提交的效果。
- 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 就会失效。
从上面的概念也能窥探出 WAL 机制的优化了,看似写了两个日志,实则是 binlog 和 redolog 都是顺序写,本身就比随机写要快很多,加上组提交两者大量的减少了 IO 的操作。
# 面试题
Q:如果你的 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO 上,可以通过哪些方法来提升性能呢?
A:针对这问题,可以考虑一下方案。
- 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 的参数,就是故意让写入等待,减少写盘次数。
- 将 sync_binlog 设置为 N,或者将 innodb_flush_log_at_trx_commit 设置为 2,就是让他们多写 page cache,但是都有丢数据的风险。
Q:执行一个 update 语句以后,我再去执行 hexdump 命令直接查看 ibd 文件内容,为什么没有看到数据有改变呢?
A:因为采用 WAL 机制,可能只写完了 redo log 或者 buffer 里,或者写入到了 page cache 里但还没有 fsync 呢。所以看不到。
Q:为什么 binlog cache 是每个线程自己维护的,而 redo log buffer 是全局共用的?
A:因为 binlog 中记录的是一个事务完整的执行语句,用来做主从备份,所以如果出现混乱可能导致事务的不一致性,然而 redolog 不需要,因为每个事务都有自己的标识比如 LSN。即使出现并发问题也没关系。
Q:事务执行期间,还没到提交阶段,如果发生 crash 的话,redo log 肯定丢了,这会不会导致主备不一致呢?
A:不会,因为二阶段提交,如果这个 redo log 已经丢失,那么 binlog 的日志还在 binlog buffer 中,还没有 fsync 呢所以备库不会不一致。
Q:如果 binlog 写完盘以后发生 crash,这时候还没给客户端答复就重启了。等客户端再重连进来,发现事务已经提交成功了,这是不是 bug?
A:不是,如果 binlog 已经写完后崩溃了,说明这个事务已经提交了,只是客户端没有收到成功的消息,但在逻辑上是已经成功了,所以恢复后看到修改成功了,是对的。