MySQL redo刷盘实测:innodb_flush_log_at_trx_commit与故障矩阵

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

有段时间,我们组老有人想把innodb_flush_log_at_trx_commit从1改成2,理由是压测能快不少。我没直接拦,自己搭了台测试机,把0、1、2三档全跑了一遍,又把"进程崩溃、断电"这两种故障到底丢不丢数据理了一遍。结论先放这儿:丢不丢,看你设几;真正"说了不丢就真不丢"的只有1,而且它没传说中那么慢。下面讲清楚为什么。

一、先分清redo落盘的两道门

MySQL靠redo log保证"提交了就不丢",这叫WAL(先写日志,再改数据页)。但redo从内存到磁盘,中间隔着两层,commit那一刻不一定全走完。

text 复制代码
事务提交 → ① 写入 log buffer(MySQL内存)
        → ② 写入 redo log 文件(OS page cache)
        → ③ fsync 刷到磁盘

第②步write是把log buffer写入OS page cache(操作系统页缓存)。第③步叫"刷盘",数据才真正落到磁盘。innodb_flush_log_at_trx_commit管的,就是提交这一刻redo要走到第几步。第③步之外的兜底刷盘,另有每秒一次的后台动作,跟这个参数是两回事。

很多人把"写入"和"刷盘"混在一起,后面看故障矩阵就会懵。先记住这道分界线,下面的三档才看得懂。

二、三档取值,到底差在哪

取值 提交时做什么 兜底刷盘 一句话风险
0 什么都不做 每秒刷一次 MySQL进程崩溃也可能丢最近约1秒
1 写入并fsync刷盘 无需兜底 不丢
2 只写入,不fsync 每秒刷一次 MySQL崩溃不丢,断电丢最近约1秒

取值0:提交时redo还留在MySQL内存的log buffer里,靠每秒一次的后台动作往外搬。这个档连进程崩溃都赌,一旦崩溃发生在两次搬运之间,已提交的事务会丢。

取值1:每个事务提交都把redo写入并fsync到磁盘。只要磁盘没坏,提交了就是真落盘了,断电也不丢。这是最稳的一档。

取值2:提交时把redo写入操作系统page cache,但不fsync。MySQL进程崩了,数据已经在文件里,OS会继续落盘,不丢。但要是操作系统崩溃或直接断电,page cache里没来得及落盘的那部分就没了。

注意0和2的差别。同样是"最多丢约1秒",2只赌断电和操作系统崩溃,0连MySQL自己崩溃都赌。这两档看着像,安全等级不一样。

三、实测:三档性能差多少

光说机制不过瘾,我直接在测试机上跑了。环境是单机MySQL 8.0.36,4核8G,SSD,为了只测redo这一个变量,先把binlog关掉了。压测用的sysbench自带写负载,表里先灌100万行,每轮跑60秒,测写入TPS。

bash 复制代码
sysbench oltp_write_only \
  --table-size=1000000 --threads=64 --time=60 \
  --mysql-db=test run

三档结果:

取值 单线程TPS 64线程TPS
0 约2100 约14200
1 约480 约13500
2 约2050 约14000

单线程下差距很扎眼。1档每个提交都fsync一次,一次fsync几毫秒,吞吐直接被压到四百多。0和2不做fsync,跑得飞快。

但把并发拉到64线程,1档只比2档慢一点点。原因叫group commit(组提交)。高并发下,一大批事务几乎同时到达,InnoDB会把它们凑成一拨,共享同一次fsync,而不是一个事务一次。fsync的总次数没有跟着事务数线性涨,单次fsync的成本被摊薄了。

想看fsync到底发生了多少次,可以查这个状态变量:

sql 复制代码
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';

1档跑60秒的fsync次数,远小于提交的事务数,就是这个道理。所以"1档一定慢到没法用"是个误解,单线程才是它的最差工况。

四、故障场景:什么情况真的会丢

性能看完,回到最关心的问题:设成0或2,到底什么时候丢?把故障分两类看,矩阵很清楚。

参数 MySQL进程崩溃 操作系统崩溃 / 断电
0 最多丢约1秒 最多丢约1秒
1 不丢 不丢
2 不丢 最多丢约1秒

MySQL崩溃和断电不是一回事。取值2扛得住前者,扛不住后者。很多团队以为"2就是双保险",其实它只保了进程这一层。

还有两个高频混淆点,顺手讲清。

第一,doublewrite(双写)跟这个参数没关系。双写buffer防的是"数据页写到一半断电"导致的半页损坏,redo防的是"已提交事务不丢"。有人把双写当成redo的兜底,方向就错了。

第二,开了binlog之后,持久性不是redo一家说了算。一次提交要走两阶段:先写redo的prepare,再写binlog,最后redo的commit。binlog没落盘的话,崩溃恢复时为了数据和日志一致,可能把binlog里缺失的事务回滚掉。所以真正想不丢,得innodb_flush_log_at_trx_commit=1sync_binlog=1一起上,圈里叫"双1"。只改redo那一半,口子还在。

五、生产环境到底该设几

理完机制和实验,选型就落到业务容忍度上。

核心链路,订单、支付、余额、对账这种,没有第二种答案,双1。一次fsync的代价,远小于一次资损的代价。就算单线程慢,靠group commit和加并发也能把吞吐拉回来。

能接受"断电丢最近约1秒"的,用2。很多日志、IM、非核心读多写少库是这类。MySQL进程崩溃不丢这一点,已经覆盖了绝大多数故障。注意这里说的是约1秒,具体窗口受innodb_flush_log_at_timeout影响,默认就是1秒。

取值0我一般不推荐。它和2的性能差距很小,却把安全底线从"断电才丢"降到了"进程崩溃也丢",这笔买卖不划算。除非你明确知道自己在赌什么。

如果你已经在用1,又嫌慢,先别急着降到0、2。先看是不是没有享受到group commit:比如用了连接池但每个连接串行提交、或者单线程批量导数据。这种场景调参数不如改并发。

sql 复制代码
-- 查看当前取值
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'sync_binlog';

-- 测试时临时改,重启后失效
SET GLOBAL innodb_flush_log_at_trx_commit = 2;

要持久化,得写进my.cnf的mysqld段再重启。测试归测试,别把SET GLOBAL当生产配置。

避坑清单

改完参数忘了持久化,是最常见的坑。SET GLOBAL只对当前实例生效,一重启就回到配置文件里的值。生产要改,先改my.cnf,再reload或计划内重启,别让测试值悄悄上线。

别把doublewrite和redo刷盘混为一谈。有人看innodb_flush_log_at_trx_commit=1就以为万事大吉,其实双写关没关、binlog的sync_binlog是不是1,都会影响"提交不丢"这个承诺。真要验收,把三个变量一起查一遍。

动这个参数之前,先回答一个问题:你的业务能不能接受断电丢最近1秒?能回答"能"再谈性能,不能就老老实实双1。我见过有人为了压测数字好看把支付库改成2,差点出事。参数好改,数据丢了你赔不起。

我的判断

数据库里很多参数,都是拿可靠性换性能。innodb_flush_log_at_trx_commit是其中最直白的一个,取值0、1、2,代表你愿意为速度赌掉多少可靠性。

我的态度是:先算清楚那1秒值多少钱,再决定要不要省。group commit已经把1档的代价压得很低,多数场景根本轮不到你去降档。与其研究怎么把1改成2,不如把连接池并发和批量提交做好。

丢数据这种事,一次就够你记住一辈子。别让一个参数,成为事故的注脚。

你用的几?有没有因为调这个参数翻过车?评论区聊聊,我猜不少人是被"改成2快一倍"这句话带偏过。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关推荐
知码研习1 小时前
Windows MySQL8.0.44 保姆级超详细安装配置教程(含 ZIP 免安装、MSI 图形安装、完整卸载流程)
mysql
ByteRock1 小时前
ClickHouse 表的生老“并”死:表实例、表元数据与并发 DDL
数据库
哈__1 小时前
破除数据库排障碎片化:一体化全链路故障根因诊断实践
数据库
充电zcx1 小时前
Linux:2:Linux下的基本指令
linux·运维·服务器
数安旭说1 小时前
从“人治”到“平台化”:迪康端点安全一体化管理平台的任务协同实战
运维·数据安全·软件需求·企业管理·终端管理·终端安全·终端运维
严同学正在努力1 小时前
SQL Server 15.0.2000.5(2019 CU5)性能分析基线构建实战教程
数据库·ai·oracle·dba
handler011 小时前
【Linux】虚拟地址空间解析
linux·运维·c++·线程·进程·虚拟地址空间·虚拟地址
MC丶科1 小时前
软考架构师90天冲刺|DAY44·Redis高级应用
数据库·数据仓库·redis·缓存·oracle·容器·规格说明书
可视化运维管理爱好者2 小时前
机房工勘记录器分享
运维·网络·nvisual