MySQL有保证数据不会丢的能力。这个能力依赖的就是redo log和binlog两个日志:
- 通过binlog,能够恢复到任何时间点的状态。
- 通过redo log ,保证MySQL(Innodb引擎)在任何时间段奔溃,重启后之前提交的记录都不会丢失。
Innodb引擎的crash-safe能力,是通过引擎层的redo log 来实现的。只要redo log 和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复。
本文详细介绍了MySQL 的 crash-safe 原理,阅读本文前,建议先阅读
MySQL系列1和 MySQL系列2 ,对MySQL架构和日志(特别是二进制日志binlog,重做日志redo log)有些基础的理解。
1 MySQL架构
如下图所示,MySQL 主要分为 Server 层和存储引擎层:
1.1 Server 层
所有跨存储引擎的功能都在这一层实现,主要有以下模块:
- 连接器: 身份认证和权限相关。
- 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除)。
- 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
- 优化器: 按照 MySQL 认为最优的方案去执行。根据cost开销进行优化,比如走哪条索引、哪个表驱动,选择cost开销最小的执行计划去执行。
- 执行器: 执行语句,然后从存储引擎返回数据。
此外,有一个通用的日志模块** binglog** 日志模块。
1.2 存储引擎
主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。从 MySQL 5.5.5 版本开始InnoDB就被当做默认存储引擎了。
2 核心日志模块
2.1 重做日志 redo log
redo log 存在于InnoDB 引擎中,redo log的引入主要是为了实现MySQL的crash-safe能力。即如果Mysql 进程异常重启了,系统会自动去检查redo log,将未写入到Mysql的数据从redo log恢复到MySQL中去。
WAL机制
WAL(Write Ahead Log)技术,也称为日志先行技术,是一种数据安全写入机制。就是先写日志,然后在写入磁盘,这样保证数据的安全性。Mysql中的Redo Log就是采用WAL机制。
Mysql中如果为了保证数据的持久性,在每提交一个事务就将日志刷新到磁盘上,这样效率就太低了,严重影响性能,所以就有了Write-Ahead 。
WAL工作机制:先在内存中提交事务,然后写日志(在InnoDB中就是Redo Log,日志是为了防止宕机导致内存数据丢失),然后再后台任务中把内存中的数据异步刷到磁盘。
redo log恢复过程
当数据库发生异常重启时,系统会自动定位到上次checkpoint的位置,同时,每个数据页中也存在一个LSN,当redo log中的LSN大于数据页中的LSN时,说明重启前redo log中的数据未完全写入数据页中,那么将从数据页中记录的LSN开始,从redo log中恢复数据。
比如redolog 的LSN 是 13000,数据库页的LSN是 10000,那么说明重启前有部分数据未完全刷入到磁盘的数据页中,那么系统将会恢复redo log 中LSN从10000开始到13000的记录到数据页中。
2.2 二进制日志 binlog
binlog 存在于Mysql Server层中,主要用于数据恢复;当数据被误删时,可以通过上一次的全量备份数据加上某段时间的binlog将数据恢复到指定的某个时间点的数据。
3 两阶段提交
为了保证binlog 和 redolog两份日志最终恢复到数据库的数据是一致的,采用两阶段提交的机制。
3.1 MySQL中一条数据更新的流程
我们以下列更新语句,来看在MySQL中数据更新的流程
update T set c=c+1 where ID=2;
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内 存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的 一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状 态,更新完成。
3.2 两阶段提交的必要性
假设redo log和binlog分别提交,可能会造成用日志恢复出来的数据和原来数据不一致的情况。
情况一: 先写redolog再写binlog
如果在一条语句redolog之后崩溃了,binlog则没有记录这条语句。系统在用binlog恢复从库时便会少了这一次的修改。恢复的从库少了这条更新。
情况二:先写binlog再写redolog
如果在一条语句binlog之后崩溃了,redolog则没有记录这条语句(可以理解为主库没有)。系统在用binlog恢复从库时便会多了这一次的修改。恢复的从库多了这条更新。
由此可见,如果不使用两阶段提交,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
3.3 两阶段提交崩溃恢复
采用两阶段提交,在做崩溃恢复(Crash recovery)时分为以下3种情况:
情况一: binlog无记录,redolog状态prepare
由于 binlog 还没写,redo log 处于 prepare 状态还没提交,所以崩溃恢复的时候,这个事务会回滚,此时 binlog 还没写,所以也不会传到备库。
情况二:binlog有记录,redolog状态prepare
redolog 中的日志是不完整的,处于 prepare 状态,还没有提交,那么恢复的时候,首先检查 binlog 中的事务是否存在并且完整,如果存在且完整,则直接提交事务,如果不存在或者不完整,则回滚事务。
情况三:binlog有记录,redolog状态commit
直接提交事务,不需要恢复。
3.4 双一设置原理
redo log 的写入机制
事务在执行过程中,生成的 redo log 是要先写到 redo log buffer 的
为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它 有三种可能取值:
- 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ; 每隔一秒把log buffer刷到文件系统中(os buffer)去,并且调用文件系统的“flush”操作将缓存刷新到磁盘上去。也就是说一秒之前的日志都保存在日志缓冲区,也就是内存上,如果机器宕掉,可能丢失1秒的事务数据
- 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;每次事务提交的时候,都把log buffer刷到文件系统中(os buffer)去,并且调用文件系统的“flush”操作将缓存刷新到磁盘上去。这样的话,数据库对IO的要求就非常高了,如果底层的硬件提供的IOPS比较差,那么MySQL数据库的并发很快就会由于硬件IO的问题而无法提升
- 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。每次事务提交的时候会把log buffer刷到文件系统中去,但并不会立即刷写到磁盘。如果只是MySQL数据库挂掉了,由于文件系统没有问题,那么对应的事务数据并没有丢失。这样的好处,减少了事务数据丢失的概率,而对底层硬件的IO要求也没有那么高(log buffer写到文件系统中,一般只是从log buffer的内存转移的文件系统的内存缓存中,对底层IO没有压力)
binlog 的写入机制
事务执行过程中,先把binlog日志写到 binlog cache,事务提交 的时候,再把 binlog cache 写到 binlog 文件中。一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入(参数 binlog_cache_size 用于binlog cache 所占内存的大小。如果超过了,就要暂存到磁盘)。
write 和 fsync 的时机,是由参数 sync_binlog 控制的:
- sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
- sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
- sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
“双 1”配置
通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。
如图所示,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog:
- innodb_flush_log_at_trx_commit 设置为1时,表示每次事务提交redo log都直接持久化到磁盘
- sync_binlog 设置为1时,表示每次事务的binlog都持久化到磁盘
“双一”设置,保证binlog和redolog持久化到磁盘,这样可以保证mysql异常重启之后数据不丢失,binlog不丢失。从而 保证MySQL数据不丢,即使异常或服务器cash, 它也能确保重启后可以恢复数据。