3. 版本对齐策略:双写策略、读写分离策略、影子表策略、并行版本策略
数据库迁移的核心挑战在于:如何在不中断服务、不丢失数据的前提下,让 API 版本与数据库版本平滑对齐。本章深入剖析四种经过生产验证的版本对齐策略,帮助架构师根据业务场景选择最合适的迁移路径。
3.1 双写策略(Dual-Write)
双写策略要求应用层在事务中同时向旧表和新表写入数据,确保两端数据实时一致。该策略适用于增量数据同步场景,通常配合历史数据回填(Backfill)完成全量迁移。
核心原则:先写旧表,再写新表;写新表失败时回滚旧表写入,保证原子性。
3.1.1 双写流程
- 开启双写:在 API 层增加新表写入逻辑,旧表写入保持不变。
- 历史数据回填:将旧表存量数据分批迁移至新表,使用时间戳或游标分页。
- 数据校验:对比新旧表记录数、校验和(Checksum),修复不一致。
- 切换读流量:将读流量逐步从旧表切至新表,观察业务指标。
- 关闭旧表写入:确认新表稳定后,移除旧表写入逻辑,完成迁移。
3.1.2 双写策略对比
| 维度 | 优势 | 劣势 |
|---|---|---|
| 数据一致性 | 实时双写,增量数据几乎无延迟 | 分布式事务复杂,可能引入最终一致性问题 |
| 回滚难度 | 旧表始终可用,回滚只需关闭新写 | 双写期间业务代码侵入性高 |
| 性能影响 | 写操作延迟增加(额外一次写入) | 高并发场景下可能成为瓶颈 |
| 适用场景 | 需要零停机、实时增量同步 | 不适合超大表(回填时间过长) |
最佳实践:双写策略建议配合 异步对账 机制,通过离线任务定期校验新旧表数据,发现不一致时自动触发补偿写入。
3.2 读写分离策略(Read-Write Splitting)
读写分离策略将写流量保留在旧库,读流量逐步切换至新库。该策略适用于读多写少的场景,通过降低读库压力来平滑迁移。
3.2.1 架构要点
- 写库(旧库):承担所有写入操作,保持数据源不变。
- 读库(新库):从旧库同步数据(如 Binlog 订阅),承担查询请求。
- 路由层:在 API 或数据访问层根据操作类型(读/写)分发请求。
3.2.2 读写分离迁移步骤
- 搭建新库:从旧库全量备份恢复,开启 Binlog 实时同步。
- 配置路由:在数据访问层(如 MyBatis、Hibernate)或代理层(如 ShardingSphere)配置读写分离规则。
- 灰度切读:先切 10% 读流量到新库,观察延迟和正确性。
- 逐步放量:按 30% → 60% → 100% 逐步切换读流量。
- 最终切换:新库稳定后,将写流量也切至新库,关闭旧库。
注意:读写分离策略要求新库与旧库的数据模型完全兼容,否则读库返回的数据格式可能与 API 预期不符。建议在路由层增加字段映射或版本转换逻辑。
3.3 影子表策略(Shadow Table)
影子表策略在同一数据库实例中创建结构不同的新表(影子表),通过数据库触发器或应用层双写将数据同步至影子表。该策略适用于表结构变更(如拆分字段、修改索引)场景。
3.3.1 影子表 vs 双写策略
| 对比维度 | 影子表策略 | 双写策略 |
|---|---|---|
| 数据存储 | 同一数据库实例,不同表名 | 不同数据库实例或不同表 |
| 同步方式 | 数据库触发器 / 应用层双写 | 应用层双写 + 异步对账 |
| 回滚速度 | 秒级(删除影子表即可) | 分钟级(需关闭新写逻辑) |
| 性能影响 | 触发器增加主表写入延迟 | 应用层双写增加网络开销 |
| 适用场景 | 表结构重构、索引优化 | 跨库迁移、分库分表 |
3.3.2 影子表迁移流程
- 创建影子表:如原表
user,创建影子表user_shadow,新结构包含拆分后的字段。 - 建立触发器:在
user表上创建 INSERT/UPDATE/DELETE 触发器,同步数据至user_shadow。 - 历史数据迁移:使用
INSERT INTO user_shadow SELECT ...批量迁移存量数据。 - 应用切换:修改 API 数据访问层,将查询和写入目标从
user改为user_shadow。 - 清理:确认无误后,删除原表和触发器,将影子表重命名为原表名。
-- 创建影子表示例
CREATE TABLE user_shadow (
id BIGINT PRIMARY KEY,
username VARCHAR(64),
email VARCHAR(128), -- 新增字段
phone VARCHAR(20), -- 从原表拆分
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- 创建触发器同步数据
CREATE TRIGGER trg_user_insert
AFTER INSERT ON user
FOR EACH ROW
BEGIN
INSERT INTO user_shadow (id, username, email, phone, created_at, updated_at)
VALUES (NEW.id, NEW.username, NEW.email, NEW.phone, NOW(), NOW());
END;
提示:影子表策略的触发器会带来约 5%-15% 的写入性能损耗。建议在业务低峰期进行历史数据迁移,并监控主库的 QPS 和慢查询。
3.4 并行版本策略(Parallel Version)
并行版本策略允许多个数据库版本同时在线,API 根据请求版本号路由到对应版本的数据库。该策略适用于多版本 API 共存、灰度发布和A/B 测试场景。
3.4.1 版本路由机制
- 请求版本标识:通过 HTTP Header(如
X-API-Version)或 URL 路径(如/v2/users)传递版本号。 - 版本映射表:维护
API 版本 → 数据库版本 → 连接串的映射关系。 - 数据隔离:不同版本的数据可存储在不同表、不同 Schema 或不同数据库实例中。
3.4.2 并行版本生命周期
| 阶段 | 操作 | 版本数量 | 风险等级 |
|---|---|---|---|
| 开发期 | 创建新版本数据库,API 路由指向新库 | 2 个(旧 + 新) | 低 |
| 灰度期 | 按用户 ID / 地域灰度切换部分流量到新版本 | 2 个 | 中 |
| 稳定期 | 新版本覆盖 100% 流量,旧版本保留只读 | 2 个 | 低 |
| 下线期 | 确认无回滚需求,删除旧版本数据库和路由 | 1 个 | 低 |
3.4.3 版本映射配置示例
# 版本映射表(YAML 格式)
version-mapping:
v1:
database: legacy_db
connection: jdbc:mysql://host1:3306/legacy
status: read-only # 只读,仅支持旧 API 查询
v2:
database: new_db_v2
connection: jdbc:mysql://host2:3306/new_v2
status: read-write # 读写,灰度流量
v3:
database: new_db_v3
connection: jdbc:mysql://host3:3306/new_v3
status: read-write # 读写,最新版本
关键风险:并行版本策略需要维护多份数据一致性。如果不同版本的数据存在双向同步,可能引发数据冲突。建议采用单向同步(旧 → 新)或版本隔离(各版本独立写入,不互相同步)。
3.5 策略选型决策矩阵
| 业务场景 | 推荐策略 | 核心考量 |
|---|---|---|
| 表结构重构(拆分字段、修改类型) | 影子表策略 | 同一实例,回滚快,触发器同步 |
| 跨库迁移(分库分表、换数据库类型) | 双写策略 | 实时增量同步,需对账机制 |
| 读流量优化(只读副本、缓存预热) | 读写分离策略 | 读多写少,逐步切读 |
| 多版本 API 共存(A/B 测试、灰度发布) | 并行版本策略 | 版本路由,数据隔离 |
| 紧急回滚需求高 | 影子表策略 / 并行版本策略 | 保留旧版本,秒级回滚 |
| 数据一致性要求极高(金融、交易) | 双写策略 + 分布式事务 | 强一致性,但性能损耗大 |
架构师建议:没有银弹策略。实际迁移中常采用组合策略——例如先用并行版本策略做 API 灰度,内部用双写策略同步数据,最后用影子表策略完成表结构变更。关键在于可观测性:每个策略都必须配套监控、告警和回滚预案。