MySQL Crash Safe
MySQL 是如何保证数据不丢的?
WAL 机制
只要 redo log 和 binlog 保证持久化到磁盘,就能确保 MySQL 异常重启后,数据可以恢复。
update 语句的执行流程
update T set c = c + 1 where ID = 2;
binlog 的写入机制
MySQL Server 层自己的日志,称为 binlog (归档日志),binlog 本身实现不了 crash safe 的能力 (不是两阶段提交,只是为了归档),binlog 是逻辑日志,可以追加写。
其实,binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size
用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 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
。
redo log 写入机制
redo log 是 InnoDB 引擎的日志,其是物理日志,循环写。
事务在执行过程中,生成的 redo log 是要先写到 redo log buffer 的。redo log 可能存在的三种状态:
为了控制 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 占用的空间即将达到
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 里的日志一起持久化到磁盘。