云计算百科
云计算领域专业知识百科平台

MySQL进程突然结束,已提交和未提交的数据会怎样?做一次崩溃恢复实验

一笔更新已经 COMMIT,另一笔刚执行完 UPDATE,还没有提交。

这时 MySQL 进程突然没了。重启以后,第一笔会不会丢?第二笔又会不会因为数据已经改过,被误当成成功?

旧提纲里,Redo 的刷盘、事务、崩溃恢复放在一起,容易最后只剩一句“日志保证安全”。我们把两笔更新分开,在专用实例里实际中断一次进程,看看恢复后的数据。

只在独立实验实例做。不要对业务数据库尝试强制结束进程。 这次验证的是数据库进程崩溃,不是拔电源,也不是模拟磁盘损坏。

先把两种事务状态做出来

实际环境是 Ubuntu 24.04 中的 MySQL 8.0.46。程序新建空数据目录,禁止 TCP,只用它自己的本地套接字访问,启动时固定 innodb_flush_log_at_trx_commit=1,并关闭 binlog。这篇只观察 InnoDB 本地恢复,不顺带推导复制或 binlog 的行为。

先开 A、B、C 三个真正独立的持续连接,默认 autocommit=1。C 初始化专用实验库;库名若已存在就换名字,不要删除已有库:

SELECT @@innodb_flush_log_at_trx_commit, @@autocommit, @@log_bin;
CREATE DATABASE blog_process_crash_lab;
USE blog_process_crash_lab;
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 1000), (2, 1000);
SELECT id, balance FROM account ORDER BY id;

第一行的三个配置值是 1、1、0。两条记录都是 1000。

在 A 把第一条改为 1100,等提交成功返回:

USE blog_process_crash_lab;
START TRANSACTION;
UPDATE account SET balance=1100 WHERE id=1;
COMMIT;
SELECT balance FROM account WHERE id=1;

A 得到 1100。

再在 B 修改第二条,但不提交,也不关闭连接:

USE blog_process_crash_lab;
START TRANSACTION;
UPDATE account SET balance=900 WHERE id=2;
SELECT balance FROM account WHERE id=2;

B 自己能查到 900。这只能说明它在自己的事务里看到了修改,不等于别人已经认可这次更新。

用 C 看一下:

SELECT id, balance FROM account ORDER BY id;
SELECT COUNT(*)
FROM information_schema.INNODB_TRX
WHERE trx_rows_modified > 0;

C 查到 1→1100、2→1000,还有一笔修改了数据、尚未结束的事务。实例里没有其他业务,所以这笔就是 B;验证程序还会核对它对应的连接编号,而不是只数个数。

现在停在这里:

A:1号余额改成1100
COMMIT成功返回

B:2号余额改成900
没有COMMIT,连接还在

C看见:1号1100,2号1000

A、B 操作不同的行,避免把“第二笔被锁住没执行”错当成“已经修改但尚未提交”。

不走正常关机,再用同一个目录启动

这一步由实验程序执行:它持有自己创建的 mysqld 子进程句柄,只对这个子进程发送 SIGKILL,等待它确实退出,再用同一个数据目录重新启动。

不是先退出 B,再停止服务。那样 B 可能已经因为连接正常关闭而回滚,观察到的就不是这次想验证的状态。

也不是调用正常关闭命令。正常关闭和突然中断,会走不同的收尾路径。

A已提交,B未提交
|
SIGKILL
|
旧mysqld进程确实退出
|
同一数据目录重新启动

程序没有用 pkill mysqld 这类按名称批量结束进程的方式,也不会从用户现有实例里找一个来中断。数据目录只初始化一次;如果重启时重新初始化一个空目录,这个实验就失去意义了。

重启后的结果

新建连接 D,在重启后的同一个实例查询:

USE blog_process_crash_lab;
SELECT id, balance FROM account ORDER BY id;

本次实际得到:

id balance
1 1100
2 1000

已经确认提交的第一笔保留下来,第二笔没有变成已提交的数据。

还要确认这个实例不是“只能查,不能继续用”。在 D 对第二条记录做一笔新的更新并提交:

START TRANSACTION;
UPDATE account SET balance=1050 WHERE id=2;
COMMIT;
SELECT id, balance FROM account ORDER BY id;

得到 1→1100、2→1050。新的修改可以正常完成。

重启阶段的错误日志也单独保留并核对了恢复信息。仅仅看到进程重新启动,或者只看到一个“ready”字样,不够证明恢复后的事务结果正确;数据还要查。

为什么不是等每个数据页都写好才提交?

先想一种直接的做法:事务改过哪些数据页,提交时就把这些页都写到最终位置,写完才告诉用户成功。

一笔更新可能涉及不止一个页,而且很多页并不挨着。若每笔提交都同步等所有数据页完成写入,提交路径会更重,后台也更难把多个修改合并处理。

InnoDB 的做法不要求“提交成功”等价于“所有修改过的数据页都已写到最终位置”。它借助日志,让必要的重做信息先满足持久化要求,数据页再按自己的刷写节奏推进。日志提交参数说明

修改数据页,产生相关日志
|
提交:按配置完成日志持久化要求
|
告诉客户端成功

数据页刷写有自己的推进节奏
不要求这时所有页都已写完

本例把 innodb_flush_log_at_trx_commit 固定为 1:提交时,Redo 写入并刷到持久存储的要求是这里讨论的前提。它仍依赖操作系统和存储设备正确履行刷写语义,不能脱离这层前提说“日志绝对不会丢”。

还有一个边界:我们没有截住某个页的刷写,也没有逐页检查它在崩溃前是否落盘。所以不能仅凭 1100 保留下来,就宣称“这一行当时肯定只在内存,后来必然全靠Redo恢复”。这次验证了进程中断后的结果,不是为每个页拍了一张落盘现场照。

Redo重做了修改,未提交的900怎么没留下?

容易产生的一个误解是:Redo 只会记录提交过的事务,所以没提交的 B 根本不会出现在日志里。

不能这样理解。未提交事务产生的页修改,也可能已经有相关 Redo 被写出,数据页也不能被简单理解成“事务提交前永远不会刷”。因此恢复不能只做一件事:“把看见的页修改都恢复,然后全部当成功”。

InnoDB 的恢复还要处理未完成事务,并利用 Undo 撤销不应保留为已提交结果的修改。InnoDB 恢复说明

恢复需要的数据页状态
|
结合事务状态处理未完成事务
|
未提交的修改不能变成有效提交

本例查询结果:2号仍是1000

这里是帮助理解职责的简图,不是一份完整的启动时序图。真实恢复有多个阶段,回滚也可能在后台推进,不能把简图当成“一定所有回滚结束后才接收任何连接”。

同样,Redo 也不是把这几条 UPDATE SQL 原样再执行一遍。别把页相关的恢复信息,和复制里常提到的语句重放混在一起。

这次没有验证什么?

我们强制结束的是数据库进程,操作系统还在运行,操作系统的缓存没有因为拔电源而消失。磁盘也没有被故意弄坏。这与整机掉电、控制器缓存丢失、页只写了一半,不是同一种故障。

所以本例没有验证 doublewrite 如何修复撕裂页,也没有比较提交参数 0、1、2 的断电丢失概率。一次进程崩溃没丢数据,不能证明所有配置断电都安全。

另一个没有模拟的是:服务已经提交成功,但成功响应在到达客户端前丢了。这时客户端只看到超时,不能马上断言事务失败,更不能盲目重试扣款;需要业务流水、幂等和结果查询。我们这里明确先等 A 的 COMMIT 成功返回,再中断进程,排除了这个歧义。

验证程序怎样安排

程序新建随机名称的实验目录,并以新的空数据目录初始化实例。A、B、C 是持续的三个 MySQL 客户端进程,按正文片段发送 SQL,不把一串命令拆成互不相干的短连接。

中断前同时核对 A 的提交后值、B 自己读到的未提交值、C 看到的已提交值,以及 B 仍在事务列表中。只有这些状态都对上,才对自己创建的服务器子进程发出不可正常收尾的终止信号。

旧服务器退出后,程序才关闭旧客户端并启动新服务器,不提前让 B 正常断开。新服务器沿用同一数据目录,不删日志、不重新初始化、不尝试强制恢复参数。D 再核对全部行,并执行后续更新。

这些检查分别防止了几种“看似测过”的错误:中断了错误的实例、B 早已回滚、其实走了正常关闭、重启了一个空库、只看启动成功却不查数据。实验的可信度在这些对应关系里,不在最后打印一句“通过”。

赞(0)
未经允许不得转载:网硕互联帮助中心 » MySQL进程突然结束,已提交和未提交的数据会怎样?做一次崩溃恢复实验
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!