一个并不安全的MySQL升级经历
摘要
讲述了一次MySQL升级,由于复制过程中AUTO_INCREMENT的行为导致数据不一致,并探讨了二进制日志格式对表引用的影响。
暂无内容
查看缓存全文
缓存时间: 2026/08/29 21:39
# 一次并不安全的 MySQL 安全升级
来源:https://blog.elis.cc/articles/a-safe-mysql-upgrade-that-wasnt-so-safe/
我收到通知,我的数据库版本已停止支持,必须立即升级。AWS 的扩展支持费用是促使我尽快升级的好动力。
我有一个可用的绿色副本,于是我升级了它,检查一切正常,然后进行了切换。
快捷又轻松,对吧?
我也曾这么认为。然而,事实并非一切正确。一小时后,我收到奇怪的 bug 报告,于是检查数据库,发现一个特定表(我们称之为*表 X*)的 ID 分配顺序异常。在旧数据库中 ID 为 1 的行,现在变成了 ID 26。
这个表在另外 6 个表中被引用,其中 5 个表正确引用了新 ID,但有一个表不知何故使用了旧数据库的 ID,现在这些 ID 指向了完全不同的行。
这真是令人费解。事情怎么会变得如此混乱?
## 迁移过程
不久前,在升级之前,执行了一个迁移操作,为*表 X* 添加了新的自增主键:
```sql
ALTER TABLE X ADD COLUMN id INT NOT NULL AUTO_INCREMENT PRIMARY KEY;
```
迁移还更新了 6 个相关表,让它们引用新的 ID 而非旧 ID。每个表的更新大致如下:
```sql
UPDATE some_table
JOIN x
ON x.old_id = some_table.x_old_id
SET some_table.x_id = x.id;
```
## AUTO_INCREMENT 列
事实证明,向复制表添加 `AUTO_INCREMENT` 列可能导致源表和副本表的行获得不同的 ID。
根据 MySQL 关于复制与 AUTO_INCREMENT 的文档 (https://dev.mysql.com/doc/refman/8.0/en/replication-features-auto-increment.html),通过 `ALTER TABLE` 添加 `AUTO_INCREMENT` 列可能不会在源和副本上产生相同的行排序。ID 的分配顺序取决于存储引擎和行处理顺序。
好吧,我当时并不知道这一点。
但为什么新字段在 6 个表中有 5 个能正确引用,而另一个表却完全出错了?
## 二进制日志格式
这是更令人困惑的地方。
MySQL 复制使用“二进制日志 (https://dev.mysql.com/doc/refman/8.0/en/binary-log.html)”记录源上发生的更改。然后这些更改被发送到副本,副本用它们来重现相同的事务。
记录的内容以及在副本上应用的方式取决于二进制日志的“格式 (https://dev.mysql.com/doc/refman/8.0/en/binary-log-formats.html)”。MySQL 支持 3 种“格式”:
- `STATEMENT`:SQL 语句本身被写入二进制日志,副本执行该语句。
- `ROW`:对单行的更改被写入二进制日志,这些行更改直接应用到副本。
- `MIXED`:在混合日志记录中,默认使用基于语句的日志记录,但在某些情况下会自动切换到基于行的记录模式。
显然,我的源数据库配置了 `binlog_format` 为 `MIXED`。
因此,那 5 个表能正确引用新 ID 的原因,是它们的更新语句是使用 `STATEMENT` 模式复制的。完全相同的 `UPDATE` 语句在副本上再次执行。它查找副本本地的 `x.id` 版本,并将正确的本地 ID 写入每个相关表。
但对于剩下的那个表,MySQL 决定改用 `ROW` 模式……在这种模式下,副本不会运行原始的 `UPDATE` 语句。它接收来自源的行变更结果,并直接应用。
因此,源上生成的 `x_id` 值被复制到了副本。但由于*表 X* 在副本上具有不同的 ID,这些值现在指向了完全不同的行。
你可能会问,为什么 MySQL 只对一个表决定使用 `ROW` 模式?
我唯一能找到的这个表与其他 5 个表的区别在于,这个表有一个 `AUTO_INCREMENT` 列。
MySQL 确实有特定情况涉及 `AUTO_INCREMENT`,它认为这对基于语句的复制不安全,因此在 `MIXED` 模式下会使用 `ROW` 格式记录。
这真是讽刺,不是吗?一切似乎最终都归结于 `AUTO_INCREMENT` 功能。
## 结论
请小心对待 MySQL 副本。这类问题的可怕之处在于它不可预期且容易被忽略,但它可能迅速演变成生产环境灾难,让你最终疑惑事情是如何变成这样的。
相似文章
SQL:设计上的缺陷
本文分析了 SQL 中固有的并发缺陷,如原子性失效、TOCTOU 问题和死锁,并通过资金转账示例展示了正确的锁机制和事务实践。
再次审视SQLite的WAL-Reset漏洞
本文讨论了SQLite WAL-reset机制中一个长期存在的漏洞,该漏洞导致数据丢失和损坏。通过一个100行的C语言工作负载进行复现,以展示竞争条件。
@KirkDBorne: 数据库的新时代来了!看看MySQL的新功能和未来方向:https://odbms.org/blog/2026/07/a-new-e…
Oracle宣布MySQL进入新时代,将先前商业版的功能迁移到社区版,并引入治理模型以重建社区信任,同时发布MySQL 9.7.0 LTS。
在大服务器上对现代Postgres和MySQL进行写入密集型sysbench测试
本文对比了在大服务器上现代PostgreSQL(版本15-19)和MySQL 8.4的写入密集型sysbench性能,发现InnoDB通常在写入吞吐量方面优于PostgreSQL,且变化较小。
我们是否更害怕可序列化隔离级别而非微妙的bug?(2024)
本文认为,默认使用较弱的数据库隔离级别是一种过早优化,并推荐使用可序列化隔离级别,除非数据库管理系统已经默认使用该级别。文章引用了导致经济损失的并发bug的真实案例。