微服务的数据所有权应该如何划分?
简化版
数据应由负责对应业务不变量的服务独占写入,其他服务通过 API、事件或只读投影获取,而不是跨库直接改表。所有权包含 Schema、写规则、质量、迁移和故障恢复责任,不等于“这张表部署在哪台机器”。
详细版
先按限界上下文识别谁有权决定状态变化,例如订单服务拥有订单生命周期,库存服务拥有可售量。跨服务流程用命令与事件协调;报表和搜索建立派生副本,并标注新鲜度与可重建性。
共享数据库阶段可先做到账号和 Schema 隔离、禁止跨域写,再逐步迁移物理存储。跨域查询用组合 API、CQRS 投影或离线数仓,避免把 join 便利性换成发布耦合和不可审计写入。
完整版教学
一、所有权由不变量决定
谁能判断一条状态迁移是否合法,谁就应拥有该数据。库存不能由订单服务直接 UPDATE stock,因为库存服务才知道预占、释放、超卖和仓库维度规则;物理表名不是划分依据。
心法:一个事实只能有一个权威写入者,可以有很多受控的读取副本。
二、读与写要分开看
写:Caller -> Owner API/Command -> owner database
读:Caller -> Owner API | local projection | analytics store
禁止跨域写不代表所有读取都必须实时 RPC。高频展示可订阅事件维护本地投影,但必须接受延迟、处理重复并提供重建和对账。
三、为什么共享表危险
| 做法 | 短期收益 | 长期问题 |
|---|---|---|
| 跨服务直接写 | 开发快 | 绕过不变量与审计 |
| 跨库 Join | 查询方便 | Schema 发布互锁 |
| 复制数据 | 本地读取快 | 最终一致与修复成本 |
| Owner API | 语义明确 | 延迟与可用性依赖 |
当两个服务都修改同一列时,无法判断谁负责数据错误,回滚一个服务也可能破坏另一个服务的新格式。
四、跨服务事务怎么处理
订单创建与库存预占属于两个所有者,通常用 Saga:订单先进入 PENDING,库存成功后确认,失败则取消。它不提供瞬时原子性,而是用状态机、幂等命令和补偿把流程收敛。
若业务必须在一个事务里维护强不变量,可能说明边界切错,应考虑把相关状态放回同一服务,而非默认引入复杂分布式事务。
五、派生数据如何治理
搜索索引、缓存和报表是派生副本,不应反向成为权威来源。事件带 entityId、version 和 eventId;消费者只推进更高版本,并通过快照或重放恢复缺口。
例如投影延迟 30 秒,页面应明确允许旧读还是回源;不能一边宣称实时库存,一边只依赖分钟级同步。
六、共享数据库如何渐进迁移
先盘点表级读写者,为每个表指定 owner;再给非 owner 撤销写权限,以 API 替代写入。随后用 CDC/双读校验迁移数据,冻结旧写入口,切换读取,最后才删除旧表。
迁移需要按记录数、校验差异和最大同步 lag 验收。例如 1000 万行回填后不能只比总数,还要抽样字段并核对按主键分桶的校验和。
删除旧写权限前应先以审计模式记录越权调用,找出定时任务、脚本和报表等不在服务拓扑里的隐藏写入者。
数据 owner 还负责保留、删除和合规请求,不能把这些责任留给共享数据库管理员兜底。
七、常见误区与追问
- 误区:一个服务一套数据库就是数据所有权。 仍可能通过共享账号跨库改写。
- 误区:复制数据后可以任意修改。 派生副本应由权威事件或重建流程更新。
- 误区:禁止 Join 就只能多次同步调用。 可用组合 API、投影和数仓。
- 误区:Saga 能恢复强原子性。 中间状态可见,补偿也可能失败。
- 追问:谁拥有客户地址? 取决于哪个上下文定义语义;订单快照与客户当前地址不是同一事实。
- 追问:如何发现越权写? 独立账号、数据库审计、血缘扫描和权限测试。
- 追问:Owner 故障时能否写副本? 不能随意双主,应按明确容灾与 fencing 切换。
八、加强记忆
数据所有权按“不变量—权威写入者—受控读取—派生副本—迁移责任”回答。核心不是数据库拆开,而是写权限和语义责任唯一;跨域流程接受明确的一致性模型,副本可重建可对账,共享库则按先禁写、再迁移、后清理演进。