17611538698
info@21cto.com

MySQL 存在 20 年的 Bug #11472 终于修复了

数据库 0 11 13小时前
图片

导读:沉淀了20年的MySQL老Bug,终于消灭了。

话说,经过 20 余年、1600 多个 Reddit 点赞、生日庆祝帖以及至少一次推迟的求婚,MySQL Bug #11472 终于被修复了。

这不仅仅是一个修复漏洞的故事,还成为业界里的一个案例研究,它揭示了数据库引擎深处的一个设计决策如何悄无声息地破坏数据完整性长达二十年之久。

这也提醒我们,最隐蔽的漏洞往往最为危险。

让我们来分析一下这个漏洞是什么,为什么它存在了 20 年,以及修复它对你的应用程序意味着什么。


漏洞:当 CASCADE 停止工作时,这是一个看起来完全无害的场景:


-- Parent table: users
CREATETABLEusers(
id INT PRIMARY KEY,
name VARCHAR(100),
status VARCHAR(20)
);

-- Child table: orders, with a CASCADE
CREATE TABLE orders(
id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
FOREIGN KEY(user_id)REFERENCESusers(id)
ONDELETECASCADE
);

-- Audit trigger: log every deletion
CREATE TRIGGER audit_order_delete
AFTER DELETE ON orders
FOREACH ROW
INSERT INTO audit_log(table_name,row_id,action,timestamp)
VALUES('orders',OLD.id,'DELETE',NOW());

现在运行以下命令:
-- Delete a user; CASCADE should delete their orders
DELETE FROM users WHEREid=42;

预期结果:用户被删除。其订单被级联删除。audit_order_delete每个被删除的订单都会触发一次触发器。

实际发生的情况(持续了20年):订单被删除。触发器从未触发。没有审计日志条目。数据悄无声息地丢失了。


架构的学问:这一切是如何发生的


MySQL 会在触发器执行之前处理外键级联。执行流程如下所示:

MySQL CASCADE 执行流程图显示了绕过

外键级联系统完全绕过了触发器基础架构。它使用了一条内部行删除路径,该路径不经过标准DELETE处理程序,因此连接到标准处理程序的触发器永远不会被调用。

这种架构决策在理论上是合理的(FK级联是内部操作,对吧?),但在实践中却严重违反了“最小惊讶原则”。


这在生产过程中造成了什么破坏


级联触发差距并非理论上的。它悄无声息地破坏了三个关键模式:

1. 审计跟踪


所有依赖触发器进行审计日志记录的系统都存在隐形漏洞。当父记录被删除时,级联删除的子记录不会留下任何审计痕迹:

Expected audit log:
  [2026-05-27 14:02:01] orders | #8823 | DELETE
  [2026-05-27 14:02:01] orders | #8824 | DELETE
  [2026-05-27 14:02:01] orders | #8825 | DELETE

Actual audit log (pre-fix):
  (empty; triggers never fired)

2. 触发器中的业务逻辑

如果团队在子表触发器中编码了验证或副作用,这些触发器会被静默跳过:

-- This trigger NEVER fired on cascade deletes
CREATETRIGGERrefund_order
AFTERDELETEONorders
FOREACHROW
BEGIN
IFOLD.status='paid'THEN
INSERTINTOrefunds(order_id,amount,reason)
VALUES(OLD.id,OLD.amount,'user_deleted');
ENDIF;
END;

删除账户的用户会连锁删除订单,但永远不会发放退款,因为触发器从未运行过。

3. 反规范化和缓存


用于维护非规范化字段或缓存表的触发器悄然失去同步:

-- Keeps a running total, silently falls out of sync
CREATETRIGGERupdate_user_total
AFTERDELETEONorders
FOREACHROW
UPDATEusersSETtotal_spent=total_spent-OLD.amount
WHEREid=OLD.user_id;

级联删除后,users.total_spent即使订单已消失,该内容仍保持不变。

为什么花了20年才修复


其实并非因为 MySQL 工程师偷懒不在乎。而是因为要正确修复这个问题,就必须触及引擎中最敏感的部分:

该修复方案需要协调三个子系统。

每个子系统都存在风险。草率的修复可能导致:

  • 破坏依赖触发器但触发器触发的现有应用程序
  • 在高并发工作负载中引发死锁(外键级联 + 触发器 = 复杂的锁定机制)
  • 创建无限循环(触发器触发 → 外键级联 → 触发器触发 → ...)


MySQL 团队在 9.7 版本中提出的解决方案非常巧妙:新增一个系统变量foreign_key_checks_trigger(默认值:OFF),用于显式启用新行为。这意味着现有应用程序即使使用了变通方法也不会受到影响,而新应用程序则可以获得正确的行为。

-- Enable trigger execution on FK cascades (MySQL 9.7+)
SETforeign_key_checks_trigger=ON;

-- Now triggers fire correctly
DELETEFROMusersWHEREid=42;
-- audit_order_delete trigger fires for each cascaded order ✓

人性的一面:网络传说

Bug #11472 不仅仅是一个 bug,它还成了开发者文化的一部分。下图是事件时间线:

漏洞 #11472 时间线:2005-2026 年

最初于 2005 年提交该bug的开发者至今仍然活跃,并在 2026 年对修复程序发表了评论,等待了二十年。

然而在修复方案讨论帖中,Reddit 上点赞最多的评论是什么?

“搞什么鬼?我一直很依赖这个功能!请改回来。” 987 个赞

这大概是开发者幽默的巅峰之作。


这对你的应用程序意味着什么


如果运行的是 MySQL 9.7 以下版本,你仍然存在这个漏洞。在执行外键级联操作期间,子表上的触发器会被静默跳过。请检查你的应用程序:


-- Find tables with BOTH triggers AND foreign key relationships
SELECT
t.TABLE_NAME,
tr.TRIGGER_NAME,
tr.EVENT_MANIPULATION,
k.REFERENCED_TABLE_NAME,
k.COLUMN_NAME
FROMinformation_schema.TRIGGERStr
JOINinformation_schema.TABLEStONtr.EVENT_OBJECT_TABLE=t.TABLE_NAME
JOINinformation_schema.KEY_COLUMN_USAGEk
ONt.TABLE_NAME=k.TABLE_NAME
ANDk.REFERENCED_TABLE_NAMEISNOTNULL
WHEREt.TABLE_SCHEMA='your_database';

如果你正在迁移到 MySQL 9.7

此修复方案为可选方案。你现有的代码不会失效,但你应该:

  1. 审核你的触发器:确定哪些触发器依赖于外键级联行为
  2. 测试方法foreign_key_checks_trigger = ON确保您的触发器能够正确处理级联事件。
  3. 注意性能:在写入量大的工作负载下,FK 级联 + 触发器可能会增加锁定开销。
  4. 更新你的临时解决方案:如果你编写了应用程序级别的代码来弥补这个漏洞,现在可以将其移除。


迁移路径


-- Phase 1: Enable in a staging environment
SETGLOBALforeign_key_checks_trigger=ON;

-- Phase 2: Monitor for issues
-- Check slow query log for new trigger-related queries
-- Watch for deadlocks in FK-heavy workloads

-- Phase 3: Enable in production (gradual rollout)
-- Start with read replicas, then primary

更大的教训

Bug #11472 揭示了软件架构的一个重要特征:静默的故障比响亮的故障更糟糕。

如果 MySQL 在级联操作中触发器无法触发时抛出错误,所有 DBA 都会立即注意到。

这个 bug 本来应该在 2005 年就被修复了。但由于它悄无声息地失败了(行被删除,触发器被跳过,表面上一切正常),所以它“存活”了二十年之久。

这与我们在其他案例中看到的模式相同:

  • 分布式系统会静默丢弃消息
  • API客户端重试但不记录失败日志
  • 能够忽略约束冲突的数据库 ORM


沉默不是一种特质,而是一颗定时炸弹。



要点总结


  1. Bug #11472 导致 MySQL 触发器在 FK CASCADE 操作期间静默跳过执行。行已被删除,但触发器从来没有触发。

  2. MySQL 9.7 中的修复方案是可选的foreign_key_checks_trigger现有应用程序不会受到影响,但应该审核触发器依赖项。

  3. 架构问题在于绕过: FK 级联使用内部删除路径,该路径不通过标准触发调度程序路由。

  4. 20 年的Bug生存经验表明,深埋地下的设计决策会造成持续存在的、不易察觉的漏洞。

  5. 审核你的审计跟踪记录。如果使用触发器进行日志记录,请确保它们确实针对所有删除路径触发,而不仅仅是直接的 DELETE 操作。


结语

新版本的MySQL 9.7 的发行说明中包含了此修复的完整细节。

如果你正在运行带有触发器和外键的生产环境 MySQL,那么这次升级正是我们期待已久的!

作者:万能的大雄

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信