【MySQL 读书笔记】当我们在执行更新语句的时候我们在做什么

该篇其实重点涉及两个日志的使用和处理。

一个是 server 层的 binlog 一个是服务器层的 redolog。

首先还是根据主线来介绍当我们在执行更新语句的时候我们在做什么

Redo Log

MySQL 使用一种叫 WAL(Write-Ahead Logging) 的技术,它达到的效果是,先写日志后写磁盘。

在计算机科学中,预写式日志(Write-ahead logging,縮寫WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。 在使用WAL的系统中,所有的修改在提交之前都要先写入log文件中。

在 MySQL 中,我们在执行一条更新语句的时候会先将记录写到 redo log 里面并更新内存,这个时候更新就算完成了。之后 InnoDB 会在适当的时候把这个操作记录更新到磁盘里面。(多提一嘴,后面会介绍,其实在更新之前还写了 undo log)redo log 在刷新之前会被记录到 buffer 中,这个 buffer 大小由 MySQL 参数 Innodb_log_buffer 控制,默认大小是 8M。

之后会通过三种方式刷新到磁盘:

1,Master Thread 每秒一次执行刷新Innodb_log_buffer到重做日志文件。
2,每个事务提交时会将重做日志刷新到重做日志文件。
3,当重做日志缓存可用空间少于一半时,重做日志缓存被刷新到重做日志文件。

被我们刷新到磁盘的 redolog 通常长这个样子:

【MySQL 读书笔记】当我们在执行更新语句的时候我们在做什么

可以看到是固定大小 512M 的文件,这取决于我们设置的参数 innodb_log_file_size 的大小。默认情况下 innodb_log_files_in_group 参数为 2 会为我们循环保存两个日志文件。保存的默认路径就是数据存放路径。

与写内存 buffer 差不多,写到磁盘的 redolog 也是会循环覆盖的。

【MySQL 读书笔记】当我们在执行更新语句的时候我们在做什么

wirte pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。 checkoutpoint 就是当前要擦除的位置,也是往后推移并且循环的。擦除记录前要把记录更新到真正的数据文件中。绿色的部分是还没有写满的部分,如果 write pos 追上了 check point 就表示写满了,这个时候不能再执行更新,必须停下来把数据刷进数据文件,然后把 check point 往后移。有了这个机制就可以保证 Innodb 发生异常重启当前提交记录不会丢失,因为事务开始的时候就已经被记录进了 redo log 中了。

Binlog Log

上面一直谈的 redo log 其实是存储引擎层的 crash-safe 机制。binlog 是在 server 层的机制,是跨存储引擎层的机制。redolog 记录的是物理上的在哪个数据页上修改了什么数据,而 binlog 是记录的执行了什么语句修改了什么数据。binlog 并不会像 redolog 那样覆盖写,他会一直以增量文件的形式存在。默认单个 binlog 可以写 1g 文件,写满 1g 会另开新的文件继续写入。 binlog 用来重放数据和做 slave 非常的方便好用。

Update 的执行

mysql> update T set c=c+1 where ID=2;

1. 执行器先找到 id=2 这一行,如果 id=2 这一行本来就在内存中,就直接返回给执行器,不然需要从磁盘读入内存,然后再返回。

2. 记录 c=0 到 undo log 然后执行器拿到引擎给的数据加 1,再调用新的接口写入这行数据。

3. 引擎将这行新的数据更新到内存中,同时将这个更新操作记录到 redo log 中,此时 redo log 处于 preprare 状态。然后告知执行器处理完成,随时可以提交事务。

4. 执行器负责生成这个操作的 binlog 并把 binlog 写入磁盘。

5. 执行调用引擎提交事务接口,引擎把刚刚写入的 redo log 改为提交状态更新完成。

【MySQL 读书笔记】当我们在执行更新语句的时候我们在做什么

可能我们都注意到了 redo log 有两个状态,preparre 和 commit 。这就是著名的两阶段提交。

要进行两阶段提交的目的就是最终让两个日志文件,也就是 redo log 和 binglog 在逻辑上保持一致。如果不进行两阶段提交,不管是先写 redo log 还是 binlog 都会可能出现不一致的情况。

先写 redo log 再写 binlog :

c = 0
mysql> update T set c=c+1 where ID=2;

假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启,由于我们之前谈到过 redo log 写完之后系统即使崩溃,仍然能够把数据恢复回来,所以恢复之后由于 redolog 写完了我们恢复回来 c 应该等于 1 。但是由于 binlog 没有写完就 crash 了,这个时候 binlog 里面就没有这个更新语句的记录,因此,之后备份日志的时候,存起来的 binlog 就没有这个语句。之后如果使用 binlog 来恢复临时库就会发生不一致。

先写 binlog 后写 redo log:

假设在 binlog 写完之后 crash, 由于 rerdo log 还没有写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0.但是 binlog 里面已经记录了 把 c +1 这条更新记录的日志。所以 binlog 来恢复的时候就多出一个语句,与原库同样不一致。

在这种情况下,如果我们是两阶段提交,我们来分析下面几种场景:

1. prepare 阶段,redo log 落盘前,MySQL crash
这种情况由于 redo log 都还没有落盘,事务并没有正确写入磁盘,数据的一致性会有问题。
 
2. prepare 阶段,redo log 落盘后,binlog 落盘前 MySQL crash
这种情况下 redo log 已经落盘,但是 binlog 却没有写入,会通过执行回滚来保证数据库的一致性。
 
3. commit 阶段,binlog 落盘后,但 commit 前 MySQL crash

这种情况下我们可以搜集到 binlog 落盘后的信息可以直接提交 binlog event。

简单的总结一下就是服务器 crash 后 MySQL 内部事务如果 binlog 已经落盘则事务应该被提交,如果 binlog 未落盘事务应该被回滚。

不得不再提一点,跟这一块紧密关联的还有两个地方,为了保证严格的数据不丢失,我们应该设置 innodb_flush_log_at_trx_commit 和 sync_binlog 为 1。

innodb_flush_log_at_trx_commit:

innodb_flush_log_at_trx_commit=0,事务发生过程,日志一直积攒在redo log buffer中,跟其他设置一样,但是在事务提交时,不产生 redo 写操作,而是 MySQL 内部每秒操作一次,从 redo log buffer,把数据写入到系统中去。如果发生 crash,即丢失1s内的事务修改操作。
innodb_flush_log_at_trx_commit=1,每次 commit 都会把 redo log 从 redo log buffer 写入到 system,并 fsync 刷新到磁盘文件中。
innodb_flush_log_at_trx_commit=2,每次事务提交时 MySQL 会把日志从 redo log buffe 写入到 system,但只写入到 file system buffer,由系统内部来 fsync 到磁盘文件。如果数据库实例crash,不会丢失redo log,但是如果服务器 crash,由于 file system buffer 还来不及 fsync 到磁盘文件,所以会丢失这一部分的数据。
所以我们设置成 1,每次 commit 都从 log buffer 里面拿出来写入到 system 然后刷到磁盘中去。

sync_binlog:

默认,sync_binlog=0,表示 MySQL不控制 binlog 的刷新,由文件系统自己控制它的缓存的刷新。这时候的性能是最好的,但是风险也是最大的。因为一旦系统Crash,在 binlog_cache 中的所有binlog 信息都会被丢失。

如果sync_binlog>0,表示每sync_binlog次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。最安全的就是 sync_binlog=1 了,表示每次事务提交,MySQL 都会把 binlog 刷下去,是最安全但是性能损耗最大的设置。这样的话,在数据库所在的主机操作系统损坏或者突然掉电的情况下,系统才有可能丢失1个事务的数据。但是 binlog 虽然是顺序IO,但是设置 sync_binlog=1,多个事务同时提交,同样很大的影响 MySQL 和 IO 性能。虽然可以通过 group commit 的补丁缓解,但是刷新的频率过高对 IO 的影响也非常大。对于高并发事务的系统来说,"sync_binlog" 设置为 0 和设置为1的系统写入性能差距可能高达 5 倍甚至更多。

最后还想补充一点我之前遇到的一个坑, MySQL 在刷新 binlog 到磁盘的时候完全有可能是该事务还没有提交的时候,即使你把 sync_binlog 设置成 1 也可能出现这种情况。就像定期刷新或者当一个事务提交了执行刷盘操作,也带上了还没有提交 commit 但是却完成了更新的操作。这一点比较容易发生在 master slave 同步上面,当 master 操作了更新, slave 去同步类似的操作, binlog 里面明明已经看到更新操作了,也去更新,但是去 master 上面查询相关的数据竟然还是旧的值没有被更新

上一篇:2017 年的 人生 hard 模式终于结束了,2018年回归初心


下一篇:ORM(二)