binlog 和 redo log,前者负责数据库数据的全量备份和恢复,可以在主从之间保证数据一致性,后者负责数据库事务更新数据的备份和恢复,可以在数据库异常崩溃重启后将已提交事务未持久化到磁盘的数据更新恢复,两者相辅相成,共同拱卫 MySQL 数据库数据的持久性和一致性。
关于 redo log 的底层写入机制,我们已经在数据库事务中详细介绍过,今天这篇教程,我们主要来看看 binlog 的底层写入机制,以及如何优化数据库写入事务性能。
注:我们知道,redo log 是重做日志,所以起名 redo,那 binlog 是啥含义呢?其实早期版本的 MySQL 只维护了 binlog,它会将所有 DDL 和 DML 语句以 Event 形式记录到一个二进制日志(binary log)中,这个 binary log 就被简称为 binlog。
binlog 底层写入机制
既然涉及到 redo log,那对应的底层存储引擎肯定就是 InnoDB 了,无论是对事务支持,还是并发性能(锁机制)上,InnoDB 都是相对于 MyISAM 引擎更好的选择,所以目前主流的 MySQL 数据库存储引擎都会选择 InnoDB,除非你的数据库主要就是读操作。所以接下来,我们依然基于 InnoDB 引擎来介绍 binlog 写入机制。
和 redo log 一样,binlog 也只关心数据库更新操作(插入、删除、更新),查询操作不对对数据库有更改,所以记录下来也没有意义。
binlog 的写入逻辑要比 redo log 简单许多,和所有数据库数据和日志更新操作一样,为了性能考虑,binlog 也不会直接写入磁盘,MySQL 会为每个线程分配独立的 binlog cache(内存块,类似 redo log buffer,分配时机是在事务开启后第一次执行 DDL/DML 语句,然后将所有更新操作先记录到 binlog cache。
这个 binlog cache 大小是有上限的,可以通过 binlog_cache_size
配置项调节,默认是 32 KB:
如果超过这个限制,会将 binlog cache 中的缓存日志刷新到存储在磁盘里的 binlog 临时文件(redo log buffer 则是在超过容量一半时直接刷新到 redo log),同时清空 binlog cache,这个临时文件的大小则可以通过 max_binlog_cache_size
配置项配置:
其默认值是很大的,一般来说不会超过这个限制,如果超过了,系统会报错。
事务提交后会将 binlog cache 和 binlog 临时文件的所有日志持久化到磁盘里的 binlog 二进制文件,也就存储 MySQL 数据的目录下:
其中 binlog.00000* 存储的是具体的日志信息,binlog.index
记录的是所有 binlog
文件的索引。你也可以在 MySQL 中通过 show binary logs
命令查看所有的 binlog 日志:
之后会清空对应的 binlog cache 和 binlog 临时文件,至此就完成了一个事务的 binlog 日志写入操作。
需要注意的是每个事务线程都有自己的 binlog cache 和 binlog 临时文件,但是会共用同一个 binlog 文件。
优化高并发写入事务性能
sync_binlog
如果更细致的划分的话,真正持久化到磁盘 binlog 日志也是分两步进行的,首先会将日志持久化达到操作系统的文件系统页面缓存(FS Page Cache),然后再根据一定的策略真正写入 binlog 磁盘文件,这个策略可以通过 sync_binlog 配置项来设置:
- sync_binlog=0:表示每次提交事务都写入到文件系统页面缓存,不持久化到 binlog 磁盘文件;
- sync_binlog=1:表示每次提交事务都会立即写入 binlog 磁盘文件;
- sync_binlog=N(N>1):表示每次提交事务都写入到文件系统页面缓存,累积到 N 个事务后才持久化到 binlog 磁盘文件。
这样做的原因当然是为了性能考虑,写入到文件系统页面缓存相较于持久化到磁盘的系统开销更小,可以忽略不计,但是也有风险,就是服务器崩溃重启后文件系统页面缓存就不存在了,但是如果只是服务器中的 MySQL 进程崩溃重启,则页面缓存中的数据依然有效,所以在高并发场景下,上述 sync_binlog 可以设置为一个大于 N 的值,比如 100-1000,其风险是服务器崩溃重启后(概率很低),其中累积的 binlog 日志会丢失。
该配置项的默认值是 1,表示每次提交事务会立即持久化到 binlog 磁盘文件:
innodb_flush_log_at_trx_commit
redo log 也有类似的机制。
我们之前介绍 redo log 时提到 redo log buffer 会在事务提交时持久化到 redo log,这是通过 innodb_flush_log_at_trx_commit
配置项默认值是 1
来控制的:
其实这个配置项还支持其他配置值:
- 0:表示每次事务提交时不将 redo log buffer 中的日志持久化到磁盘;
- 1:表示每次事务提交时都将 redo log buffer 中的日志持久化到 redo log 磁盘文件;
- 2:表示每次事务提交时都将 redo log buffer 中的日志写入到文件系统页面缓存。
InnoDB 后台进程每隔 1s 将 redo log buffer 日志刷新到 redo log 时底层就是借助了页面缓存作为中介,并不是直接持久化到 redo log 磁盘文件的。同理 innodb_flush_log_at_trx_commit
配置为 2 的时候,也存在服务器重启后累积的日志数据丢失的风险。
我们再回顾下 redo log 的两阶段提交:
如果 innodb_flush_log_at_trx_commit 配置值为 1 的话,其实在 redo log prepare 阶段就会持久化 redo 日志到磁盘文件。
优化写入事务性能
我们知道在数据库写入操作时真正涉及磁盘操作的也就是这里的两个日志写入了,更新的数据页是在内存 Buffer Pool 中完成的,所以降低了这两个日志的磁盘 IO 也就等同于优化了写入性能。
因此,我们可以通过将上面介绍的 sync_binlog 配置为 N(N > 1),innodb_flush_log_at_trx_commit
配置为 2 调整日志真正写入磁盘文件的时机来优化高并发写入事务的性能:
这样一来,我们就可以实现 binlog 和 redo log 的组提交,尽可能减少磁盘 IO,这里存在的潜在风险是第 3 步之前服务器崩溃重启后页面缓存中累积的重做日志和 binlog 日志会丢失,不能恢复这期间的更新数据,但是 MySQL 异常崩溃重启后可以不受影响。实际情况下,除了机房断电等不可控因素,基本不会出现服务器异常崩溃重启的情况。
有人可能会疑惑这里的日志写入涉及到磁盘操作,会不会影响 MySQL 性能,其实很小,因为 redo log 和 binlog 记录的信息量很小,而且都是追加写入,属于顺序 IO,只有随机 IO 是最耗性能的,比如对数据表进行更新操作就是典型的随机 IO,因为每次都要去不同的位置进行操作。这里我们将日志先写入页面缓存,再同步到磁盘文件,也会极大降低日志写入的磁盘 IO。