32.6 共享数据库反模式
一句话
共享数据库把 “数据模型” 变成了跨团队契约:任何一张表的改动都可能同时影响 5 个服务,于是没人敢改、只能加字段——它是 “分布式单体” 的根,也是微服务拆不动的真正原因。
共享库的耦合面
| 耦合 | 表现 | 后果 |
|---|---|---|
| 表结构耦合 | 多个服务读写同一张表 | 改列要全量回归 |
| 事务耦合 | 一个服务的事务里改别人的表 | 无法独立部署/回滚 |
| 语义耦合 | 同一字段在不同服务里含义不同 | “status=3” 到底是啥,没人说得清 |
| 性能耦合 | 报表查询拖慢交易库 | 一个慢 SQL 打挂核心链路 |
| 版本耦合 | 无法灰度,只能同版本发布 | 发布窗口互相绑架 |
代码:把 “共享” 变成 “契约”
-- ❌ 反模式:订单服务直接 JOIN 用户表
SELECT o.id, u.name FROM orders o JOIN users u ON u.id = o.buyer_id;
-- ✅ 方案 A:订单库冗余一份“买家快照”(读优化,事件同步)
ALTER TABLE orders ADD COLUMN buyer_name VARCHAR(64); -- 下单时写入
-- ✅ 方案 B:通过用户服务 API 批量取(缓存 + 批量接口,避免 N+1)
-- ✅ 方案 C:读侧宽表(CQRS),由投影维护,见 32.1
// 同步冗余字段:订阅 UserRenamed 事件,幂等更新(允许短暂旧值)
func (p *OrderProjection) OnUserRenamed(e UserRenamed) {
p.db.Exec(`UPDATE orders SET buyer_name=? WHERE buyer_id=?`, e.NewName, e.UserID)
}
实测:静态扫描 “跨模块 JOIN” 有多容易
一段 10 行的领域 SQL,扫描跨模块表引用:
命中:orders JOIN users → 跨模块(订单 × 用户)
命中:orders JOIN coupons → 跨模块(订单 × 营销)
结论:2 处跨模块 JOIN,均属共享数据库反模式
# 可复现:把每张表归属到模块,扫描 SQL 里的跨模块引用
grep -rniE "join +(users|coupons)" ./order/ # 命中即违反边界
这类扫描能进 CI:跨模块 JOIN 一出现就失败,比 “架构评审时人工看” 可靠得多。修复路径是 API/事件/读模型三选一(见正文方案 A/B/C)。
反模式
| 反模式 | 后果 |
|---|---|
| 多服务写同一张表 | 事务边界混乱,谁都能改坏数据 |
| 用视图“隔离”共享表 | 视图仍是同一份数据,schema 变更照样穿透 |
| 跨库 JOIN 做报表 | 交易库被拖慢;分库后 JOIN 直接不可用 |
| 把 “共享库” 当 “集成方案” | 团队间隐式耦合,无法独立发布 |
| 只拆服务不拆数据 | 分布式单体(见 30.3 分布式单体反模式) |
| 冗余字段无同步机制 | 数据永久不一致,且无人负责 |
| 只靠评审发现跨库 JOIN | 评审看的是设计稿,代码里的 JOIN 照样上线 | | 拆库后仍保留“只读老库”的后门 | 边界永久失效,谁都能绕 |
三条铁律
🔑 一张表只能有一个写 owner;其他方只读或走 API。
🔑 跨服务查询靠 API / 事件 / 读模型,不靠共享表。
🔑 冗余可以,但必须有同步与对账;没有 owner 的冗余就是脏数据。
自检
Q1 为什么 “把共享表包一层视图” 不算隔离?
▸ 答案:因为视图与表共享同一份数据与 schema:加列、改类型、改约束依然影响所有使用方;性能上也没有隔离(慢查询照样打在同一实例)。视图只是 “换个名字”,没有改变耦合关系。
Q2 拆共享库的第一步应该做什么?
▸ 答案:找出写 owner 与读依赖:每张表标记 “谁写、谁读、读哪些列”。然后按 “读依赖” 决定先给谁提供 API/事件,再谈拆库。跳过这步直接拆,会在上线后发现 “某个报表悄悄依赖了这张表”。
Q3 冗余字段(如订单里的买家名)什么时候可以接受?
▸ 答案:当它满足三条:读多写少(避免频繁同步)、允许陈旧(改名后短暂旧值可接受)、有 owner 与同步机制(事件/CDC + 对账)。金额、库存等强一致字段不要冗余,宁可走 API。
Q4 跨库报表怎么办?
▸ 答案:不要在交易库上跑:用 CDC/事件把数据同步到数仓/读模型,在分析库上做 JOIN。这样既不拖慢交易,也能承受复杂查询;代价是延迟(分钟级)与同步链路的运维。
Q5 共享库与 “中台” 的边界在哪?
▸ 答案:中台是通过 API/服务共享能力,共享库是通过表共享数据。前者有契约、可版本化、可灰度;后者没有。中台若最终靠 “大家读同一个库”,就退化成了共享库。
Q6 团队说 “拆库太贵,先共享着”,怎么评估风险?
▸ 答案:量化三个数:改一张表要协调几个团队、发布是否必须同版本、一次慢查询影响多少业务。三者任一超过阈值(如 >3 个团队、必须同版本、影响核心交易),共享库就已经在收 “协调税”,拆库的投入应该进路线图(见 58.4 演进路线图)。
Q7 为什么 “只读老库” 也会毁掉拆分?
▸ 答案:因为读也会形成依赖:老库的 schema、索引、数据质量一旦变化,读方全部受影响;更糟的是团队会用它 “绕过新服务”,让新 API 长不出真实需求。拆分后应把老库设为 “只写(过渡)→ 只读(短期)→ 下线”,并给出明确截止时间。
故障复盘(典型模式,脱敏)
| 阶段 | 现象 | 判断 | 动作 |
|---|---|---|---|
| 上线 | 用户表加字段,5 个服务同时报错 | 严格反序列化 + 共享表 | 回滚字段 |
| T+30m | 回滚后仍有 1 个服务异常 | 该服务缓存了旧 schema | 重启服务 |
| T+1d | 决定拆库,但报表直接 JOIN 用户表 | 读依赖未梳理 | 先提供 API/读模型 |
| 复盘 | 共享库 = 隐式契约,没人知道谁在读 | 依赖图谱缺失 | 建表-服务依赖清单 |
根因:一张表被多服务读写,且严格解析。代价:一次全站级故障。根治:一表一写 owner;读走 API/读模型;变更前用依赖清单评估影响面。