《数据密集型应用系统设计》派生数据系统
柏舟 新冠4年 07-01
系统中一般存在两种数据:
- 存储于事务型数据库的核心数据;
- 根据核心数据派生的派生数据,存储于分析型数据库(数据仓库)中。
从读取路径上看分为:
- 写路径:一旦数据进入,即刻完成,无论是否有人要求访问它;
- 读路径:只有当人访问时才会发生。
写路径类似于尽早求值,而读路径类似于惰性求值。
flowchart TD;
subgraph WritePath;
WriteServer --> Check["语法、内容检查"] --> WriteDB[写入数据库] --> 索引;
end
subgraph ReadPath;
ReadServer --> 查询;
end;
Read ---> ReadServer;
Write ---> WriteServer;
索引 <---> 查询;
这两种视角类似于数据结构和算法,从静态和动态的角度看待系统架构设计。
静态:数据一致性
我认为除了在《数据密集型应用系统设计》分布式系统的挑战和一致性一文中提到的那些问题。在应用层上,数据库字段的设计需要注意:
- 关系:在不需要关系搜索(比如join)的对象、树状或列表的对象可以使用json等方式使用文本存储。传统的一对多建模方式需要建立2张表,如果不需要跨表查询就可以简化设计。
- 不要有派生字段:当多个服务使用同一个表时,派生字段就会泄漏更新逻辑,极大的提高了维护难度。
- 不变性:采用不变性仅追加的设计可以简化数据的并发一致性问题。
动态:数据实时更新
数据的实时更新存在3种做法:
名称 | 方式 |
---|---|
重复计算 | 每次请求重新计算 |
流处理 | 每次写请求提交后,异步推送到消息系统,触发更新逻辑;读请求读取物化视图 |
批处理 | 定时计算或增量计算,将结果写入物化视图 |
但是各有优缺点:
名称 | 优点 | 缺点 |
---|---|---|
重复计算 | 实现简单,实时 | 当数据集较大时计算慢 |
流处理 | 实时,最终一致性,较快 | 准确性低,更新不及时 |
批处理 | 容错非常方便,准确性高 | 不实时 |
从这个角度看,缓存、索引和实体化视图主要用于调整读、写路径之间的边界,通过预先计算结果,写路径承担更多的工作,而读路径则可以简化加速。
C/S架构
类似Web应用程序的C/S架构,客户端大部分是无状态的,服务器拥有对数据的全部控制权。
此时,物化视图等状态的计算都在服务器中完成:
flowchart LR;
Client <--"every times"--> Server;
subgraph Server;
D1[Core Data] --> D2[Derived Data];
end;
但是它不会订阅服务器端的更新,除非显示的轮询更新。
有状态可离线的客户端
但类似微信聊天应用和游戏这种带有状态有需要与服务器同步的应用该如何做呢?
可以将客户端视为一个缓存/物化视图,当客户端首次初始化时,仍然需要使用读路径获取初始状态,但此后依赖于服务器发送的状态推送流。
flowchart LR;
subgraph Client;
D2[Derived Data];
end;
subgraph Server;
D1[Event] --> D2S[Derived Data];
end;
Client <-- "websocket: events" --> Server;
总结
- 流式计算 > 每次重新计算一遍 > 坏的流式计算(状态到处不一致) ≈ 数据库冗余字段。
- 如果没有特殊情况,比如数据量特别大还是每次重新计算一遍,虽然慢一点但是实现方便。
- 因为Web程序是如此的常见,现有的协议(http)和框架都对请求/响应交互逻辑有根深蒂固的假设。微服务与流处理类似,都是以服务的方式构造系统,但是微服务使用同步的请求/响应,消息的交换是双向的;而流处理使用异步的消息推送,消息的交换是单向的。