对于运行中的 SaaS 系统,数据库升级最危险的时刻,往往不是执行 SQL 的那几秒,而是新旧版本同时运行、数据正在持续写入、迁移尚未完成,却已经有部分代码开始依赖新结构的时候。一个新增字段可能影响数十个 PHP 接口,一次索引重建可能拖慢整张业务表,一次表名切换也可能让正在执行的查询遇到错误。如果系统服务多个租户,故障还可能从一个功能扩散到整个产品。
因此,线上数据库升级不能只理解为执行几条 ALTER TABLE 语句,而应当作为应用代码、数据库结构、数据迁移和发布流程共同参与的工程来设计。对于常见的 PHP、MySQL 和 InnoDB 技术栈,核心原则是让新旧版本在过渡期间尽可能兼容,并把不可逆操作推迟到确认安全之后。
一、升级前先判断变更风险
不同数据库变更的风险差别很大。增加一个允许为空的字段、增加普通索引、修改大表中的字段类型、删除旧字段,不能采用完全相同的发布方法。部分 MySQL 版本和操作支持即时或原地 DDL,另一些操作则可能重建整张表、产生大量磁盘读写,或者在最终切换时等待元数据锁。即使被称为在线 DDL,也不代表整个过程完全没有阻塞风险。
实施前应先确认数据库版本、存储引擎、表大小、索引、外键、现有触发器、复制架构和备份情况。对于订单、支付、订阅、用户权限等核心业务表,还要检查是否存在长事务、慢查询以及高峰期写入。测试环境应尽可能使用与生产环境相同的数据库版本和表结构,并通过接近真实业务的压力测试,观察变更期间的查询延迟、锁等待、磁盘空间和复制延迟。
例如,给一张大型用户表新增可空字段,通常比直接把一个被大量查询使用的字段改名更容易平滑实施。后者可能同时影响 PHP 查询、报表、后台任务和第三方接口。如果新版本发布后立即删除旧字段,尚未更新的旧 PHP 进程便可能继续查询已经不存在的列。升级计划必须把这些依赖关系提前列出来,而不是等到生产环境报错后再寻找调用位置。
二、采用扩展、迁移、切换、收缩的升级方式
对于需要修改数据结构、迁移数据或调整字段含义的复杂变更,可以采用“扩展—迁移—切换—收缩”(Expand–Migrate–Switch–Contract)流程。
第一步是扩展结构。先增加新字段或新表,暂时保留旧字段,让旧版本 PHP 代码仍然可以正常运行。新增字段时应仔细设计默认值、是否允许为空、索引和数据类型,避免一次变更引入不必要的全表重建。
第二步是发布兼容版本。PHP 代码可以先继续读取旧字段,同时开始向新字段写入,或者通过明确的兼容层同时维护新旧字段。双写并非天然安全:如果两个字段在同一事务中更新失败、某个后台任务只写入其中一个字段,或者重试逻辑不完善,就会产生数据不一致。因此,应当明确哪个字段是权威数据源,并设计失败重试、差异检测和修复流程。
第三步是分批迁移历史数据。大表不宜一次性执行没有限制的全表更新。可以根据主键范围分批处理,每批完成后提交事务,并记录已经迁移到的位置。这样可以限制单次事务的持续时间,减少锁占用、事务日志膨胀和复制延迟。迁移程序还应当能够安全重跑:重复执行同一批任务时,不应产生重复记录或覆盖更新后的业务数据。
第四步是校验和切换。不能只检查迁移脚本是否执行成功,还要比较迁移前后的记录数量、关键字段、空值比例和业务汇总结果。对于金额、订阅状态、库存或权限数据,应当进行针对性的业务校验。只有在新字段的数据完整性得到确认后,才能逐步将读取逻辑切换到新字段。
第五步是收缩结构。观察一段时间,确认所有 PHP 实例、队列消费者、定时任务和报表程序都不再依赖旧字段之后,再安排删除旧字段或旧表。删除操作通常应与前面的应用发布分开执行,避免一次发布同时包含结构扩展、数据迁移、代码切换和不可逆清理。
三、不要用简单触发器和全表复制代替完整迁移方案
下面这类代码不应直接用于生产环境:
CREATE TABLE users_new LIKE users;
ALTER TABLE users_new ADD COLUMN last_login TIMESTAMP;
INSERT INTO users_new SELECT * FROM users;
它存在一个直接问题:新旧表的字段数量或顺序发生变化后,SELECT * 可能因列数不匹配而失败;即使列数碰巧相同,也可能把数据写入错误的字段。更重要的是,复制历史数据期间,原表仍可能不断发生新增、更新和删除。只执行一次全表复制,无法保证复制结果包含期间发生的所有变化。
如果确实需要自行实现增量迁移,就必须设计完整的数据同步机制,覆盖新增、更新和删除,并处理并发写入、重复事件、迁移重试和数据校验。触发器只是可选工具,不是自动获得数据一致性的保证。还需要考虑现有触发器之间的冲突,以及同步逻辑对业务写入延迟的影响。对于多数团队,优先选择经过验证的迁移工具或由应用层维护的、可重跑的分批迁移任务,通常比临时编写触发器更容易测试和维护。
如果只是新增一个可空字段,而且当前 MySQL 版本和表结构支持相应的在线 DDL,可能无需复制整张表。例如,在确认操作兼容性后,可以考虑:
ALTER TABLE users
ADD COLUMN last_login TIMESTAMP NULL,
ALGORITHM=INSTANT;
这里的 ALGORITHM=INSTANT 并非适用于所有 MySQL 版本、表结构和 DDL 操作。如果当前环境不支持该算法,语句可能失败;不应为了让语句执行成功而未经评估地退回可能重建整张表的操作。上线前必须在相同版本和近似结构的环境中验证,并准备应急方案。
四、合理使用 pt-online-schema-change
对于某些不能通过 MySQL 原生在线 DDL 安全完成的变更,可以评估 Percona Toolkit 提供的 pt-online-schema-change。该工具通常通过创建影子表、分批复制数据、使用触发器同步变更,再通过表名切换完成升级,目的是降低长时间阻塞原表读写的风险。
执行前可以先进行检查:
pt-online-schema-change \
--user=ddl_migrator \
--ask-pass \
--host=db.example.com \
--alter="ADD COLUMN last_login TIMESTAMP NULL" \
D=app_db,t=users \
--dry-run
这只是检查示例,不会真正完成生产变更。确认工具输出、数据库版本、权限、表结构、外键关系和备份均符合要求后,才可以制定正式执行计划。真正执行时需要使用 --execute,但不能仅仅因为测试通过就直接对生产库运行。应根据实际环境配置负载阈值、复制延迟检查、操作窗口和中止条件。
该工具也有明确限制。例如,原表已有触发器时,需要检查是否能够安全处理;外键可能使表切换更复杂;表需要有合适的主键或唯一索引;复制延迟和额外磁盘占用也可能成为瓶颈。即使数据复制阶段允许业务继续读写,最终切换仍可能因为元数据锁而等待。工具能够降低风险,却不能保证锁表时间固定为几秒,也不能替代备份和恢复演练。
五、事务管理不能替代数据库结构回滚
PHP 开发中常见的错误,是把多条 DDL 语句放进同一个事务,并假设出现异常后执行 rollBack() 就能恢复原状。例如:
$pdo->beginTransaction();
try {
$pdo->exec(
"ALTER TABLE orders
ADD COLUMN delivery_status VARCHAR(20) NULL"
);
$pdo->exec(
"ALTER TABLE products
ADD COLUMN stock_threshold INT NULL"
);
$pdo->commit();
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log(
"Database upgrade failed: " . $e->getMessage()
);
throw $e;
}
虽然异常处理代码可以避免 PHP 程序无声地忽略错误,但这段代码并不能提供所期望的整体回滚保证。在 MySQL 中,ALTER TABLE 等许多 DDL 语句会触发隐式提交,不能依靠普通事务将多个结构变更作为一个可回滚的整体。因此,应当把数据库迁移设计成多个可独立检查的步骤,每一步都有明确的执行结果、重试规则和恢复计划。
对于普通业务数据写入,事务仍然非常重要。涉及订单状态、库存和支付记录时,应尽量将相互依赖的数据修改放入适当的事务,并正确处理异常、死锁和重复请求。但数据库结构升级的恢复方案应当另行设计,不能将 rollBack() 当作数据库结构的撤销按钮。
六、建立有针对性的监控与发布控制
数据库升级期间,监控系统需要关注业务和数据库两方面的指标。业务侧应观察 HTTP 错误率、接口延迟、支付或订单处理失败率、队列积压以及租户级异常;数据库侧则应关注活跃连接数、慢查询、锁等待、长事务、CPU、磁盘 I/O、剩余空间和复制延迟。对于 MySQL,可以利用 Performance Schema 检查元数据锁的持有者和等待者,帮助定位 DDL 为什么无法完成。
不要把某个指标超过固定数字直接等同于必须回滚。例如,连接数超过 100 对小型系统可能异常,对配置较大的系统却未必有问题。告警阈值应当根据正常基线、连接池配置、数据库容量和业务高峰确定。系统可以自动暂停迁移任务、停止继续发布或触发告警,但涉及结构切换和数据恢复的动作,必须依据预先验证的规则执行,不能仅凭单一指标盲目运行回滚 SQL。
Git 可以记录迁移脚本、应用版本和发布说明,但它不能替代数据库备份,也不会自动保存可恢复的数据。正式升级前,应确认备份能够恢复,必要时通过时间点恢复或其他经过验证的恢复机制制定方案。对于大型数据库,还要预留影子表、索引构建、临时日志和备份所需的磁盘空间。
七、SaaS 多租户系统需要区分代码灰度与结构迁移
SaaS 系统可以按租户逐步开放新功能,例如先让一小部分租户使用新版本,再扩大范围。这属于应用功能灰度发布,并不意味着可以简单地只为这些租户升级同一张共享数据库表的结构。如果多个租户共用一套数据库和同一张 users 表,新增字段通常是表级操作,而不是只对某个租户生效。
如果每个租户拥有独立数据库或独立 schema,就需要建立可追踪的迁移队列,记录每个租户的数据库版本、迁移状态、失败原因和重试结果,并限制并发迁移数量,避免大量结构操作同时冲击数据库。如果多个租户共用同一张表,则应先完成兼容性结构升级,再通过功能开关控制新功能的启用范围。
租户隔离本身也不能只靠迁移流程保证。PHP 查询必须在正确的授权上下文中限制租户范围,更新和删除操作同样要验证租户归属;不能因为新增了 tenant_id 字段,就认为数据泄露风险已经解决。
线上数据库升级的目标不是让每条 SQL 都绝对不锁表,而是让变更过程可预测、可观测、可暂停,并在出现问题时有明确的恢复路径。通过兼容性设计、分批迁移、数据校验、审慎切换和充分的恢复演练,PHP SaaS 系统才能在持续迭代的同时尽可能保持服务连续性。
PHP SaaS 系统数据库结构升级,如何避免线上服务中断
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP