第 1 章 —— DSM 的职责与适用边界
本章目标: 本章完成后,开发者能够根据数据责任和故障恢复方式,判断一个状态是否适合放入 DSM。
学习目标
- 用一句话说明 DSM 解决的协同问题。
- 区分业务事实、协同状态和事件历史。
- 解释为什么「可以从同伴修复」不是「可以替代数据库」。
- 对订单履约案例中的四类数据作出初步存储选择。
前置条件
- 能阅读基本 Java 接口。
- 知道数据库事务和服务多实例部署的基本含义。
- 本章不要求运行 DSM,也不要求理解复制协议。
案例进度
履约系统有多个 API 和 worker 实例。它们需要共享服务路由、订单分片所有权和请求计数,但订单、库存和支付已经有各自的事实系统。
本章只确定责任边界。第 2 章才会创建第一个 Runtime。
DSM 解决的协同问题
先看一个常见但危险的需求:
某团队认为 DSM 能把数据复制到多个节点,因此计划把订单状态迁入 DSM 并移除数据库。
问题不在于 DSM 能不能保存一个 Order
对象,而在于订单状态承担什么责任。订单是需要事务约束、审计历史和确定写入结果的业务事实。副本在网络恢复后得到相同可见值,不能自动补回一次丢失的扣款,也不能撤销已经返回给客户的成功响应。
DSM 适合另一类数据:体量较小、服务运行所需、能够从同伴修复或从权威来源重新生成的协同状态。
按数据责任划分存储边界
图中有三类责任:
- 事务数据库保存订单、库存和支付等业务事实。
- DSM Runtime 保存路由、租约和可合并计数等协同状态。
- 消息或事件系统保存需要可靠传递和历史重放的业务事件。
同一个应用可以同时使用三类系统。选择 DSM 不等于排斥数据库或消息系统。
从公开契约看 DSM
当前 DsmRuntime
是承载多个类型化集合的公开门面。它公开三种注册入口:
<E extends DsmEntity<E>> DsmRegister<E> register(CollectionSpec<E> spec);
<E extends LeaseEntity<E>> DsmLeaseRegister<E> leaseRegister(
LeaseCollectionSpec<E> spec);
<E extends DsmEntity<E>, S extends MergeableState<S>>
DsmCrdtCollection<E, S> crdt(
CrdtCollectionSpec<E, S> spec,
StateMerger<E, S> merger);这三个入口代表三种不同问题,而不是三个容量档位:
| 问题 | 首选集合 | 本书案例 |
|---|---|---|
| 一个 key 当前应该看到哪个逻辑值 | Register | route-hints |
| 当前谁拥有一项有期限的工作权 | Lease | shard-owners |
| 多个节点的本地更新怎样合并 | CRDT | request-counter |
第 5—9 章会逐一建立这些语义。当前阶段先识别业务不变量,再选择集合。
第一个可验证增量:给状态分类
本章的资产提供了 8 个候选状态。打开 classification-cases.json,在查看参考答案前逐项填写:
transactional-fact:需要事务与审计的业务事实。coordination-state:可修复或可重建的运行协同状态。event-history:需要可靠传递或历史重放的事件。analytical-data:面向大规模查询和分析的数据。
运行检查:
npm test -- --test-name-pattern="chapter 01 classification"这个命令只验证分类资产和参考答案,不运行 DSM。
拆开来看
1. 业务事实的正确性依赖提交历史
订单从 PENDING 变成
PAID,通常还伴随支付记录、库存变更和审计信息。系统需要回答「哪次事务成功」「谁修改了什么」「失败后如何补偿」。DSM
当前集合 API 不是跨资源事务协议。
因此,订单主记录、库存扣减和支付账务留在事务系统。
2. 协同状态服务于运行决策
Route hint 告诉网关当前访问哪个履约实例。它可能短暂过期,也可以由健康检查重新发布。节点漏掉一次在线 delta 时,repair 可以让副本追上当前状态。
这种状态适合 DSM,但仍要写清陈旧窗口和失败后的业务行为。
3. 事件历史需要传递保证
ChangeStream
可以让工具或应用观察集合变化,但它不是持久消息日志。需要重放全部
OrderPaid 事件时,应使用消息系统、outbox 或事件存储。
4. 大规模分析需要不同的数据模型
DSM 集合以 key 和协同语义为中心,不提供面向大量数据的 join、二级索引和分析查询。高容量日志和指标应该进入相应的数据管道。
反例与故障注入
假设把库存余量放入 Register:
- 节点 A 读取余量为 1,并返回扣减成功。
- 网络分区期间,节点 B 也读取余量为 1,并返回扣减成功。
- 分区恢复后,Register 通过确定性规则只保留一个可见值。
副本最后可以收敛,但两个客户已经收到成功响应。收敛没有撤销第二次销售,也没有产生一条可审计的库存事务。
这个反例说明,DSM 适用性取决于业务响应能否撤销、是否需要事务历史;对象能否序列化只是技术条件之一。
为什么会这样
Register 的责任是让候选状态得到确定的可见结果。它不协调多个业务资源,也不替调用方撤回已经产生的外部效果。
后续章节会解释本地提交、在线 delta 和 repair。现在先保留一句边界:
repair 修复副本状态,不修复已经对外发生的业务承诺。
调整后的状态模型
把「库存余量」改成「库存服务当前首选路由」。重新判断:
- 丢失后能否从健康检查重新生成?
- 短时间读到旧值时,调用方能否重试或切换?
- 是否需要查询完整修改历史?
- 多节点并发发布时,能否接受确定性 winner?
如果答案分别是「能、能、不需要、能」,它比库存余量更接近 DSM Register 的适用条件。
常见误区
把「内存」理解成单机缓存
DSM 的共享状态由 Runtime 管理,并可通过 sync 和 repair
在节点间传播。它不是把一个 Map 暴露给所有
JVM,也不是简单的本地缓存封装。
把「多个副本」理解成数据永不丢失
数据安全仍取决于持久化配置、节点拓扑、故障相关性、备份和恢复流程。多个内存副本不能替代数据库备份。
先选集合,再找业务理由
「CRDT 看起来最先进」不是选择依据。没有并发本地更新和可合并状态时,CRDT 只会增加模型与验证成本。
理解检查
- 服务发现地址适合 Register 的关键条件是什么?
- 为什么一个最终收敛的库存值仍可能造成超卖?
- 如果数据需要支持按客户、时间和状态组合查询,DSM 是否应该是主存储?为什么?
- 一个 worker 的当前心跳适合 DSM,不代表 worker 完成过的全部任务历史也适合 DSM。两者的责任差异是什么?
实验
将以下状态分成业务事实、协同状态、事件历史和分析数据:
- 订单当前状态。
- 履约服务路由提示。
- 分片 owner 与租约到期时间。
- 各节点请求计数。
OrderPaid事件历史。- 支付账务分录。
- 调用延迟直方图。
- 临时功能开关。
完成后与 参考答案 比较。参考答案不是机械分类:临时功能开关是否适合 DSM,还取决于它是否有外部事实源、陈旧窗口和回滚要求。
实验验收卡
| 字段 | 内容 |
|---|---|
| 运行命令 | npm test -- --test-name-pattern="chapter 01 classification" |
| 输入或故障 | 8 个候选状态及其责任说明 |
| 可观察结果 | 分类资产结构有效;参考答案覆盖全部候选状态 |
| 证据等级 | E1:只验证书籍资产与静态责任边界 |
| 本实验未证明 | DSM Runtime 可以启动、节点可以复制、任何生产容量或可用性结论 |
回顾
- DSM 保存分布式协同状态,不替代所有共享数据系统。
- 业务事实、协同状态、事件历史和分析数据需要不同的保证。
- Register、Lease 和 CRDT 分别回答最新值、所有权和可合并更新问题。
- repair 修复副本状态,不撤销已经发生的业务响应。
下一步
第 2 章将启动一个最小 DsmRuntime,创建
route-hints Register,并第一次写入和读取路由提示。