导读:沉淀了20年的MySQL老Bug,终于消灭了。
话说,经过 20 余年、1600 多个 Reddit 点赞、生日庆祝帖以及至少一次推迟的求婚,MySQL Bug #11472 终于被修复了。
这不仅仅是一个修复漏洞的故事,还成为业界里的一个案例研究,它揭示了数据库引擎深处的一个设计决策如何悄无声息地破坏数据完整性长达二十年之久。
这也提醒我们,最隐蔽的漏洞往往最为危险。
让我们来分析一下这个漏洞是什么,为什么它存在了 20 年,以及修复它对你的应用程序意味着什么。
-- 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 会在触发器执行之前处理外键级联。执行流程如下所示:
外键级联系统完全绕过了触发器基础架构。它使用了一条内部行删除路径,该路径不经过标准DELETE处理程序,因此连接到标准处理程序的触发器永远不会被调用。
这种架构决策在理论上是合理的(FK级联是内部操作,对吧?),但在实践中却严重违反了“最小惊讶原则”。
级联触发差距并非理论上的。它悄无声息地破坏了三个关键模式:
所有依赖触发器进行审计日志记录的系统都存在隐形漏洞。当父记录被删除时,级联删除的子记录不会留下任何审计痕迹:
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;
删除账户的用户会连锁删除订单,但永远不会发放退款,因为触发器从未运行过。
用于维护非规范化字段或缓存表的触发器悄然失去同步:
-- 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即使订单已消失,该内容仍保持不变。
其实并非因为 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,它还成了开发者文化的一部分。下图是事件时间线:
最初于 2005 年提交该bug的开发者至今仍然活跃,并在 2026 年对修复程序发表了评论,等待了二十年。
然而在修复方案讨论帖中,Reddit 上点赞最多的评论是什么?
“搞什么鬼?我一直很依赖这个功能!请改回来。” 987 个赞
这大概是开发者幽默的巅峰之作。
-- 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
此修复方案为可选方案。你现有的代码不会失效,但你应该:
foreign_key_checks_trigger = ON:确保您的触发器能够正确处理级联事件。-- 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 年就被修复了。但由于它悄无声息地失败了(行被删除,触发器被跳过,表面上一切正常),所以它“存活”了二十年之久。
这与我们在其他案例中看到的模式相同:
沉默不是一种特质,而是一颗定时炸弹。
Bug #11472 导致 MySQL 触发器在 FK CASCADE 操作期间静默跳过执行。行已被删除,但触发器从来没有触发。
MySQL 9.7 中的修复方案是可选的。foreign_key_checks_trigger现有应用程序不会受到影响,但应该审核触发器依赖项。
架构问题在于绕过: FK 级联使用内部删除路径,该路径不通过标准触发调度程序路由。
20 年的Bug生存经验表明,深埋地下的设计决策会造成持续存在的、不易察觉的漏洞。
审核你的审计跟踪记录。如果使用触发器进行日志记录,请确保它们确实针对所有删除路径触发,而不仅仅是直接的 DELETE 操作。
结语
新版本的MySQL 9.7 的发行说明中包含了此修复的完整细节。
如果你正在运行带有触发器和外键的生产环境 MySQL,那么这次升级正是我们期待已久的!
作者:万能的大雄
本篇文章为 @ 场长 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 微信公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
请扫描二维码,使用微信支付哦。