灰度发布过程中,如何保证新旧版本数据的一致性?如果新版本有数据库结构变更怎么办?
- 标签:后端、系统设计、场景题、数据库、灰度发布
🏛 业务架构师
灰度期间新旧版本的数据一致性——核心原则一句话:灰度期间任何时刻,新旧版本都能正确读写全量数据(-expand-contract 演进模式),做不到就别灰度,先改数据设计。
数据库结构变更的安全流程
原则:只做向后兼容的变更,分多次发布完成一次重构
- expand(扩展):新版本只加字段/表(可空、带默认值),不删不改老字段——旧版本代码对新字段无感知照常跑,新版本双写新老字段
- 迁移存量:后台任务把老字段数据刷到新字段,校验一致后进入下一步
- 灰度切读:新版本按灰度比例读新字段(读逻辑带开关,出问题切回老字段)
- contract(收缩):全量稳定后,下个版本才移除老字段——删除永远放在最后一个独立发布
禁止清单(每条都是事故)
- 修改字段含义(新版本把"状态 0=正常"改成"0=禁用"——老版本逻辑立刻错乱)
- 重命名字段(等于删 + 加同步进行)
- 非空约束直接加上(老版本插入语句没有该字段直接失败)
灰度期间的数据正确性细节
- 双写的一致性:双写失败时以老字段为准 + 告警补偿;双写期间新老字段短暂不一致要有读取策略(以谁为准)
- 写入归属:灰度比例内的新请求走新逻辑写新字段,存量数据的迁移任务与在线双写并发——迁移要幂等、可重跑,与双写的竞争以"在线写后到"为准
- MQ 消息兼容:消息结构变更,新消费者要能读老消息(默认值兜底),灰度期间两类消息共存
- 缓存结构变更:value 结构变了,版本化 cache key(
key:v2:{id}),避免新老代码互相读错缓存
收尾(架构师观点):这套流程的本质是**"把一次危险的大变更,拆成多次无风险的小变更"**——每次发布都可独立回滚。宁可发十次,不可赌一次:发布的成本以小时计,回滚失败的成本以事故计。
🏗 系统架构师
灰度期间新旧版本数据一致性是灰度发布最难的部分,我按数据面兼容 → schema 变更策略 → 不兼容变更的套路讲。
1. 原则:数据层不灰度,只做逻辑灰度 数据库是共享的,新旧版本必须同时兼容同一份 schema。真正的风险是 DDL:新版本改了表结构,回滚到老版本就崩了。所以铁律:灰度期间不做破坏性 DDL。
2. 兼容性设计(展开/收缩原则)
- 加字段:只加不改不删,带 DEFAULT NULL;新版本写新字段、读时兼容 null;老版本无视新字段——双向兼容,随时可回滚
- 改语义:禁止直接改老字段含义,走"新增字段 → 双写 → 迁移 → 切读 → 停写老字段"的五步法,每步独立发布,灰度随时可退
- 删字段:最后一步,确认所有版本不读后(代码审计 + 查询日志)再删,一般滞后一个版本周期
3. 必须做 DDL 时的策略(题眼)
text
方案A:expand-contract(推荐)
发布N版:加新列(INSTANT DDL)+ 双写新老列 + 触发器/任务回填存量
发布N+1版:读新列,观察无差异
发布N+2版:停写老列,再下一版删除老列
每步可独立回滚,灰度期永远兼容
方案B:版本化表
v2 版本写 order_v2 新表,灰度验证后迁移切换——成本高,留给超大改版
方案C:避免 DDL 的设计
预留扩展 JSON 列(MySQL JSON 字段)存演进属性,结构变更变成数据变更4. 灰度期间的数据一致校验:双写期间对账任务持续比对新老列,不一致告警;灰度用户的行为数据打版本标签,对比灰度/存量的业务指标(订单量、错误率)防逻辑分叉。
5. 回滚配套:回滚代码不回滚数据(数据永远向前兼容),所以任何 DDL 设计前先问一句——"这个变更回滚后老代码还能跑吗?",答案是否定的就上 expand-contract。
💻 全栈开发专家
核心原则一句话:灰度期间新旧版本必须共用同一套 schema,且 schema 变更保持双向兼容。用 expand-contract(扩张-收缩)三段式落地。
兼容性规则(红线)
灰度期间数据库只允许"加法":
- ✅ 加可空列 / 带默认值的列、加索引、加表
- ❌ 删列、rename 列/表、改列类型、加唯一约束、改列语义
违反任何一条,新旧版本必有一个直接报错。
Expand-Contract 三段式
sql
-- Phase 1 扩张:加新列,新旧并存
ALTER TABLE orders ADD COLUMN amount_fen DECIMAL(18,2) NULL;
-- 代码层:双写(老列新列都写),读仍用老列
-- Phase 2 迁移:后台任务按 id 分批回填
UPDATE orders SET amount_fen = amount * 100 WHERE id BETWEEN ? AND ?;
-- 校验 count(新列 <> 老列换算) = 0 后,读切新列,观察一个发布周期
-- Phase 3 收缩:老版本全下线后,再发一个版本删除老列
ALTER TABLE orders DROP COLUMN amount;配套细节
- 大表 DDL 走 gh-ost 低峰执行(另题展开),别在灰度窗口里做。
- 双写一致性:新老列写入放在同一个本地事务;补偿任务比对两列,不一致修复。
- 唯一约束的坑:想加唯一索引去重,灰度期间老代码还在写脏数据——先停写、清洗、加约束,或者用"软唯一"(应用层 + 唯一冲突重试)。
- 缓存与消息:序列化格式(Protobuf/JSON 字段)同样要向后兼容,消息加新字段没问题,消费端别用强校验。
回滚视角
灰度失败要能"只切流量、不动数据"地回滚——这就是为什么收缩阶段必须等老版本全下线:数据结构永远比代码旧一拍,才进可攻退可守。