01 · 先画数据流:事实、派生数据与架构取舍
目标:为活动报名系统划定事实源、派生数据和同步返回的边界。
从一次报名开始
我们要做一个社区活动平台:组织者发布活动,用户查询和报名,组织者查看报名数量。先假设每天 10 万次浏览、1 万次报名,热门活动会出现突发请求。这些是练习条件,不是任何产品的真实数据。
同一个“报名数量”可能出现在数据库、搜索页和日报。三个数字不同的时候,必须先知道哪个系统有权回答。事实源(system of record)保存业务认可的状态;派生数据由事实计算得到。搜索索引可以重建,但前提是原始数据、转换规则和必要的历史仍然存在。备份则用于恢复被破坏或误删的事实,不能因为有副本就省略。
报名操作只访问一个活动及少量记录,属于操作型工作负载。按城市计算全年报名趋势要扫描大量记录,属于分析型工作负载。两者可以先共用数据库;当扫描抢占在线请求的资源,才有依据把分析移到派生系统。名字叫“数据湖”不会自动带来治理,名字叫“数仓”也不会自动保证指标正确。
本课程的初始数据流如下。箭头表示依赖,而不是指定产品。
用户请求 → 报名服务 → 主数据库(活动、报名)
↓ 已提交变更
搜索视图、统计视图
↓
查询页与日报
一次成功响应承诺报名已记录,不承诺搜索页已刷新。这个区分之后会决定事务、复制和事件流如何设计。
用一个小实验区分事实和视图
保存为 architecture.py。先预测:事实增加一条记录后,已经算好的列表会自动改变吗?
def counts(rows):
result = {}
for row in rows:
activity = row["activity"]
result[activity] = result.get(activity, 0) + 1
return result
registrations = [{"id": "r1", "activity": "a1"}]
view = counts(registrations)
registrations.append({"id": "r2", "activity": "a1"})
assert view == {"a1": 1}
rebuilt = counts(registrations)
assert rebuilt == {"a1": 2}
print("cached:", view)
print("rebuilt:", rebuilt)
uv run --python 3.12 --no-project architecture.py
预期输出:
cached: {'a1': 1}
rebuilt: {'a1': 2}
counts 是一条明确的派生规则。修改事实不会重新调用函数,第二个结果来自主动重建。这里没有网络延迟;即使单机,维护多个表示也需要更新机制。
引导练习:① 把 r2 的活动改成 a2;② 重跑;③ 检查旧视图仍只有 a1: 1,新视图有两个活动且各为 1。不要通过手工修改两个字典“修好”,我们要保留可重复的派生过程。
哪些复杂度值得现在引入
单节点让事务和诊断更直接,但无法独自承受整机永久损坏。分布式可以支持容错、地域访问和扩容,同时引入部分失败。选择托管服务时,比较的不只是机器费用,还包括值班、恢复演练、迁移和退出成本。自托管意味着控制更多,也意味着自己负责维护。
云上常见的存算分离让计算节点按需读取共享持久存储,便于独立扩缩容;代价是远程读取、缓存和网络成为性能因素。微服务按团队与业务边界拆分后,跨服务操作需要协调。Serverless 改变资源分配方式,数据状态和失败语义仍需要设计。我们目前不凭预计增长就拆服务。
独立练习:浏览允许晚 5 秒更新,报名必须立即确认,日报第二天生成。把三个接口分别连到合适的数据源,并写出一个拒绝立刻分片的理由。
参考设计与检查方法
报名连到主数据库,成功以事务提交为准;浏览读取延迟受监测的视图;日报读取有明确截止时间的快照。初始负载尚未证明超出单机容量,先测量热点、扫描量和峰值并发,比立即引入分片更有依据。浏览视图超过 5 秒时,应有回源、提示或降级规则;不能继续声称满足 5 秒目标。
如果实验中的旧视图也变了,检查是否把 view 写成了原始列表的别名。如果重建仍缺数据,先打印输入记录,区分输入丢失与派生逻辑错误。
在 design.md 写下上述数据流、三个接口承诺,以及暂不引入的组件和重新评估条件。这是综合设计的第一块内容。
参考资料
- DDIA 第二版第 1 章:架构取舍,对应操作与分析、事实源、云与自托管、单节点与分布式系统。