开放问答
滚动发布期间新旧版本共存,DB 新列双写出现"旧 Pod 把新值写回 NULL",你们怎么设计 schema 变更的兼容窗口?
该问题的正文、回答与评论。
提问者 程亦舟-967 · 状态 open
SREschema迁移滚动发布双写MyBatis兼容性
背景
我做了 10 年 SRE,最近一次复盘的 P2 让我对"expand → backfill → contract"这套经典三步法产生了怀疑。场景:一个订单服务,K8s Deployment 6 副本,MySQL 8.0(InnoDB,单主单从),HikariCP 连接池。这次变更是给 t_order 加一列 refund_channel(新退款渠道枚举),同时把老列 refund_type 的语义从"渠道"拆成"渠道 + 方式",新逻辑里写 refund_channel,老列保留。
现象
发布流程是标准的 maxUnavailable=0 / maxSurge=1,灰度 1/6 → 1/3 → 全量。灰度到 1/3 那一刻,监控发现 refund_channel 出现一批 NULL:新 Pod 写入的值,几秒后被"变"成 NULL 了。查 binlog 才发现根因——老 Pod 的 ORM 实体里 refundChannel 字段是 null,UPDATE 语句把整行都 SET 了一遍(MyBatis 的 update 是整实体覆盖写),直接把新 Pod 刚写的 refund_channel 覆盖成 NULL。这不是个例,灰度窗口里 40 分钟,老 Pod 和 新 Pod 交替处理同一批订单,互相"擦屁股"。
已尝试的排查与结果
- 把 MyBatis 的 update 改成"非空字段才 SET"(
<if test="x != null">)——解决了覆盖写问题,但引入了新问题:新 Pod 想主动把某字段置 NULL(业务上合法)就做不到,得加一个forceNull标记位,代码变丑。 - 考虑"先双写、再切读":新 Pod 同时写
refund_type和refund_channel,老 Pod 只写refund_type,读侧都从refund_type读——这条路走通了,但回填窗口长达 3 周(老订单要按新口径重算),期间任何一次"误删新列"都是 P0。 - 考虑"发布前冻结写"——业务侧拒绝,说退款链路不能停。
- 讨论过"新老版本走不同表 / 不同库"——DBA 表示 6 副本 × 2 套连接池,连接数和 binlog 压力都吃不消。
期望得到的答案形态
- 你们团队在"滚动发布期间新旧版本共存"这件事上,schema 变更的兼容窗口是怎么切的?(具体到:双写多少时间、切读判据是什么、contract 那步的兜底动作)
- ORM 层的"非空才 SET"策略,你们是用全局配置、注解,还是逐 mapper 改?有没有踩过"业务上合法置 NULL"的坑,怎么解?
- 回填窗口长达数周时,你们怎么防止"提前 contract"(比如某个实习生看到新列有值就删了老列)?有没有用 schema 版本表 / 迁移状态机 / 强制 review 卡点?
- 如果让你重做一次这个变更,你会怎么排?给我一份可抄的 checklist 最好。
坐标:Java 17 / Spring Boot 3.2 / MyBatis 3.5 / MySQL 8.0.32 / K8s 1.28,单服务 6 副本,退款链路不可停。
已采纳
回答者
回答正文
评论
暂无评论。