You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 23 | MySQL是怎么保证数据不丢的
今天这篇文章我会继续和你介绍在业务高峰期临时提升性能的方法。从文章标题“MySQL是怎么保证数据不丢的你就可以看出来今天我和你介绍的方法跟数据的可靠性有关。
在专栏前面文章和答疑篇中我都着重介绍了WAL机制你可以再回顾下[第2篇](https://time.geekbang.org/column/article/68633)、[第9篇](https://time.geekbang.org/column/article/70848)、[第12篇](https://time.geekbang.org/column/article/71806)和[第15篇](https://time.geekbang.org/column/article/73161)文章中的相关内容得到的结论是只要redo log和binlog保证持久化到磁盘就能确保MySQL异常重启后数据可以恢复。
评论区有同学又继续追问redo log的写入流程是怎么样的如何保证redo log真实地写入了磁盘。那么今天我们就再一起看看MySQL写入binlog和redo log的流程。
# binlog的写入机制
其实binlog的写入逻辑比较简单事务执行过程中先把日志写到binlog cache事务提交的时候再把binlog cache写到binlog文件中。
一个事务的binlog是不能被拆开的因此不论这个事务多大也要确保一次性写入。这就涉及到了binlog cache的保存问题。
系统给binlog cache分配了一片内存每个线程一个参数 binlog\_cache\_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小就要暂存到磁盘。
事务提交的时候执行器把binlog cache里的完整事务写入到binlog中并清空binlog cache。状态如图1所示。
![](https://static001.geekbang.org/resource/image/9e/3e/9ed86644d5f39efb0efec595abb92e3e.png)
图1 binlog写盘状态
可以看到每个线程有自己binlog cache但是共用同一份binlog文件。
* 图中的write指的就是指把日志写入到文件系统的page cache并没有把数据持久化到磁盘所以速度比较快。
* 图中的fsync才是将数据持久化到磁盘的操作。一般情况下我们认为fsync才占磁盘的IOPS。
write 和fsync的时机是由参数sync\_binlog控制的
1. sync\_binlog=0的时候表示每次提交事务都只write不fsync
2. sync\_binlog=1的时候表示每次提交事务都会执行fsync
3. sync\_binlog=N(N>1)的时候表示每次提交事务都write但累积N个事务后才fsync。
因此在出现IO瓶颈的场景里将sync\_binlog设置成一个比较大的值可以提升性能。在实际的业务场景中考虑到丢失日志量的可控性一般不建议将这个参数设成0比较常见的是将其设置为100~1000中的某个数值。
但是将sync\_binlog设置为N对应的风险是如果主机发生异常重启会丢失最近N个事务的binlog日志。
# redo log的写入机制
接下来我们再说说redo log的写入机制。
在专栏的[第15篇答疑文章](https://time.geekbang.org/column/article/73161)中我给你介绍了redo log buffer。事务在执行过程中生成的redo log是要先写到redo log buffer的。
然后就有同学问了redo log buffer里面的内容是不是每次生成后都要直接持久化到磁盘呢
答案是,不需要。
如果事务执行期间MySQL发生异常重启那这部分日志就丢了。由于事务并没有提交所以这时日志丢了也不会有损失。
那么另外一个问题是事务还没提交的时候redo log buffer中的部分日志有没有可能被持久化到磁盘呢
答案是,确实会有。
这个问题要从redo log可能存在的三种状态说起。这三种状态对应的就是图2 中的三个颜色块。
![](https://static001.geekbang.org/resource/image/9d/d4/9d057f61d3962407f413deebc80526d4.png)
图2 MySQL redo log存储状态
这三种状态分别是:
1. 存在redo log buffer中物理上是在MySQL进程内存中就是图中的红色部分
2. 写到磁盘(write)但是没有持久化fsync)物理上是在文件系统的page cache里面也就是图中的黄色部分
3. 持久化到磁盘对应的是hard disk也就是图中的绿色部分。
日志写到redo log buffer是很快的wirte到page cache也差不多但是持久化到磁盘的速度就慢多了。
为了控制redo log的写入策略InnoDB提供了innodb\_flush\_log\_at\_trx\_commit参数它有三种可能取值
1. 设置为0的时候表示每次事务提交时都只是把redo log留在redo log buffer中;
2. 设置为1的时候表示每次事务提交时都将redo log直接持久化到磁盘
3. 设置为2的时候表示每次事务提交时都只是把redo log写到page cache。
InnoDB有一个后台线程每隔1秒就会把redo log buffer中的日志调用write写到文件系统的page cache然后调用fsync持久化到磁盘。
注意事务执行中间过程的redo log也是直接写在redo log buffer中的这些redo log也会被后台线程一起持久化到磁盘。也就是说一个没有提交的事务的redo log也是可能已经持久化到磁盘的。
实际上除了后台线程每秒一次的轮询操作外还有两种场景会让一个没有提交的事务的redo log写入到磁盘中。
1. **一种是redo log buffer占用的空间即将达到 innodb\_log\_buffer\_size一半的时候后台线程会主动写盘。**注意由于这个事务并没有提交所以这个写盘动作只是write而没有调用fsync也就是只留在了文件系统的page cache。
2. **另一种是并行的事务提交的时候顺带将这个事务的redo log buffer持久化到磁盘。**假设一个事务A执行到一半已经写了一些redo log到buffer中这时候有另外一个线程的事务B提交如果innodb\_flush\_log\_at\_trx\_commit设置的是1那么按照这个参数的逻辑事务B要把redo log buffer里的日志全部持久化到磁盘。这时候就会带上事务A在redo log buffer里的日志一起持久化到磁盘。
这里需要说明的是我们介绍两阶段提交的时候说过时序上redo log先prepare 再写binlog最后再把redo log commit。
如果把innodb\_flush\_log\_at\_trx\_commit设置成1那么redo log在prepare阶段就要持久化一次因为有一个崩溃恢复逻辑是要依赖于prepare 的redo log再加上binlog来恢复的。如果你印象有点儿模糊了可以再回顾下[第15篇文章](https://time.geekbang.org/column/article/73161)中的相关内容)。
每秒一次后台轮询刷盘再加上崩溃恢复这个逻辑InnoDB就认为redo log在commit的时候就不需要fsync了只会write到文件系统的page cache中就够了。
通常我们说MySQL的“双1”配置指的就是sync\_binlog和innodb\_flush\_log\_at\_trx\_commit都设置成 1。也就是说一个事务完整提交前需要等待两次刷盘一次是redo logprepare 阶段一次是binlog。
这时候你可能有一个疑问这意味着我从MySQL看到的TPS是每秒两万的话每秒就会写四万次磁盘。但是我用工具测试出来磁盘能力也就两万左右怎么能实现两万的TPS
解释这个问题就要用到组提交group commit机制了。
这里我需要先和你介绍日志逻辑序列号log sequence numberLSN的概念。LSN是单调递增的用来对应redo log的一个个写入点。每次写入长度为length的redo log LSN的值就会加上length。
LSN也会写到InnoDB的数据页中来确保数据页不会被多次执行重复的redo log。关于LSN和redo log、checkpoint的关系我会在后面的文章中详细展开。
如图3所示是三个并发事务(trx1, trx2, trx3)在prepare 阶段都写完redo log buffer持久化到磁盘的过程对应的LSN分别是50、120 和160。
![](https://static001.geekbang.org/resource/image/93/cc/933fdc052c6339de2aa3bf3f65b188cc.png)
图3 redo log 组提交
从图中可以看到,
1. trx1是第一个到达的会被选为这组的 leader
2. 等trx1要开始写盘的时候这个组里面已经有了三个事务这时候LSN也变成了160
3. trx1去写盘的时候带的就是LSN=160因此等trx1返回时所有LSN小于等于160的redo log都已经被持久化到磁盘
4. 这时候trx2和trx3就可以直接返回了。
所以一次组提交里面组员越多节约磁盘IOPS的效果越好。但如果只有单线程压测那就只能老老实实地一个事务对应一次持久化操作了。
在并发更新场景下第一个事务写完redo log buffer以后接下来这个fsync越晚调用组员可能越多节约IOPS的效果就越好。
为了让一次fsync带的组员更多MySQL有一个很有趣的优化拖时间。在介绍两阶段提交的时候我曾经给你画了一个图现在我把它截过来。
![](https://static001.geekbang.org/resource/image/98/51/98b3b4ff7b36d6d72e38029b86870551.png)
图4 两阶段提交
图中我把“写binlog”当成一个动作。但实际上写binlog是分成两步的
1. 先把binlog从binlog cache中写到磁盘上的binlog文件
2. 调用fsync持久化。
MySQL为了让组提交的效果更好把redo log做fsync的时间拖到了步骤1之后。也就是说上面的图变成了这样
![](https://static001.geekbang.org/resource/image/5a/28/5ae7d074c34bc5bd55c82781de670c28.png)
图5 两阶段提交细化
这么一来binlog也可以组提交了。在执行图5中第4步把binlog fsync到磁盘时如果有多个事务的binlog已经写完了也是一起持久化的这样也可以减少IOPS的消耗。
不过通常情况下第3步执行得会很快所以binlog的write和fsync间的间隔时间短导致能集合到一起持久化的binlog比较少因此binlog的组提交的效果通常不如redo log的效果那么好。
如果你想提升binlog组提交的效果可以通过设置 binlog\_group\_commit\_sync\_delay 和 binlog\_group\_commit\_sync\_no\_delay\_count来实现。
1. binlog\_group\_commit\_sync\_delay参数表示延迟多少微秒后才调用fsync;
2. binlog\_group\_commit\_sync\_no\_delay\_count参数表示累积多少次以后才调用fsync。
这两个条件是或的关系也就是说只要有一个满足条件就会调用fsync。
所以当binlog\_group\_commit\_sync\_delay设置为0的时候binlog\_group\_commit\_sync\_no\_delay\_count也无效了。
之前有同学在评论区问到WAL机制是减少磁盘写可是每次提交事务都要写redo log和binlog这磁盘读写次数也没变少呀
现在你就能理解了WAL机制主要得益于两个方面
1. redo log 和 binlog都是顺序写磁盘的顺序写比随机写速度要快
2. 组提交机制可以大幅度降低磁盘的IOPS消耗。
分析到这里,我们再来回答这个问题:**如果你的MySQL现在出现了性能瓶颈而且瓶颈在IO上可以通过哪些方法来提升性能呢**
针对这个问题,可以考虑以下三种方法:
1. 设置 binlog\_group\_commit\_sync\_delay 和 binlog\_group\_commit\_sync\_no\_delay\_count参数减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的因此可能会增加语句的响应时间但没有丢失数据的风险。
2. 将sync\_binlog 设置为大于1的值比较常见是100~1000。这样做的风险是主机掉电时会丢binlog日志。
3. 将innodb\_flush\_log\_at\_trx\_commit设置为2。这样做的风险是主机掉电的时候会丢数据。
我不建议你把innodb\_flush\_log\_at\_trx\_commit 设置成0。因为把这个参数设置成0表示redo log只保存在内存中这样的话MySQL本身异常重启也会丢数据风险太大。而redo log写到文件系统的page cache的速度也是很快的所以将这个参数设置成2跟设置成0其实性能差不多但这样做MySQL异常重启时就不会丢数据了相比之下风险会更小。
# 小结
在专栏的[第2篇](https://time.geekbang.org/column/article/68633)和[第15篇](https://time.geekbang.org/column/article/73161)文章中我和你分析了如果redo log和binlog是完整的MySQL是如何保证crash-safe的。今天这篇文章我着重和你介绍的是MySQL是“怎么保证redo log和binlog是完整的”。
希望这三篇文章串起来的内容能够让你对crash-safe这个概念有更清晰的理解。
之前的第15篇答疑文章发布之后有同学继续留言问到了一些跟日志相关的问题这里为了方便你回顾、学习我再集中回答一次这些问题。
**问题1**执行一个update语句以后我再去执行hexdump命令直接查看ibd文件内容为什么没有看到数据有改变呢
回答这可能是因为WAL机制的原因。update语句执行完成后InnoDB只保证写完了redo log、内存可能还没来得及将数据写到磁盘。
**问题2**为什么binlog cache是每个线程自己维护的而redo log buffer是全局共用的
回答MySQL这么设计的主要原因是binlog是不能“被打断的”。一个事务的binlog必须连续写因此要整个事务完成后再一起写到文件里。
而redo log并没有这个要求中间有生成的日志可以写到redo log buffer中。redo log buffer中的内容还能“搭便车”其他事务提交的时候可以被一起写到磁盘中。
**问题3**事务执行期间还没到提交阶段如果发生crash的话redo log肯定丢了这会不会导致主备不一致呢
回答不会。因为这时候binlog 也还在binlog cache里没发给备库。crash以后redo log和binlog都没有了从业务角度看这个事务也没有提交所以数据是一致的。
**问题4**如果binlog写完盘以后发生crash这时候还没给客户端答复就重启了。等客户端再重连进来发现事务已经提交成功了这是不是bug
回答:不是。
你可以设想一下更极端的情况整个事务都提交成功了redo log commit完成了备库也收到binlog并执行了。但是主库和客户端网络断开了导致事务成功的包返回不回去这时候客户端也会收到“网络断开”的异常。这种也只能算是事务成功的不能认为是bug。
实际上数据库的crash-safe保证的是
1. 如果客户端收到事务成功的消息,事务就一定持久化了;
2. 如果客户端收到事务失败(比如主键冲突、回滚等)的消息,事务就一定失败了;
3. 如果客户端收到“执行异常”的消息,应用需要重连后通过查询当前状态来继续后续的逻辑。此时数据库只需要保证内部(数据和日志之间,主库和备库之间)一致就可以了。
最后,又到了课后问题时间。
今天我留给你的思考题是你的生产库设置的是“双1”吗 如果平时是的话你有在什么场景下改成过“非双1”吗你的这个操作又是基于什么决定的
另外,我们都知道这些设置可能有损,如果发生了异常,你的止损方案是什么?
你可以把你的理解或者经验写在留言区,我会在下一篇文章的末尾选取有趣的评论和你一起分享和分析。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
# 上期问题时间
我在上篇文章最后,想要你分享的是线上“救火”的经验。
@Long 同学,在留言中提到了几个很好的场景。
* 其中第3个问题“如果一个数据库是被客户端的压力打满导致无法响应的重启数据库是没用的。”说明他很好地思考了。
这个问题是因为重启之后业务请求还会再发。而且由于是重启buffer pool被清空可能会导致语句执行得更慢。
* 他提到的第4个问题也很典型。有时候一个表上会出现多个单字段索引而且往往这是因为运维工程师对索引原理不够清晰做的设计这样就可能出现优化器选择索引合并算法的现象。但实际上索引合并算法的效率并不好。而通过将其中的一个索引改成联合索引的方法是一个很好的应对方案。
还有其他几个同学提到的问题场景,也很好,很值得你一看。
> @Max 同学提到一个很好的例子客户端程序的连接器连接完成后会做一些诸如show columns的操作在短连接模式下这个影响就非常大了。
> 这个提醒我们在review项目的时候不止要review我们自己业务的代码也要review连接器的行为。一般做法就是在测试环境把general\_log打开用业务行为触发连接然后通过general log分析连接器的行为。
> @Manjusaka 同学的留言中,第二点提得非常好:如果你的数据库请求模式直接对应于客户请求,这往往是一个危险的设计。因为客户行为不可控,可能突然因为你们公司的一个运营推广,压力暴增,这样很容易把数据库打挂。
> 在设计模型里面设计一层,专门负责管理请求和数据库服务资源,对于比较重要和大流量的业务,是一个好的设计方向。
> @Vincent 同学提了一个好问题用文中提到的DDL方案会导致binlog里面少了这个DDL语句后续影响备份恢复的功能。由于需要另一个知识点主备同步协议我放在后面的文章中说明。