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/读模型;变更前用依赖清单评估影响面。

关联