English version: Trading app architecture: six constraints and what they force。本系列共六篇。
交易客户端的架构,没法靠品味来挑。它的大半在你写第一行代码之前就已经定了:数据变化有多快、一笔下错的单值多少钱、目录有多大、你有几个平台和几个团队,以及这个应用运行在一台你不掌控的设备上。
所以有用的问题不是“选哪个技术栈”,而是:每一条约束各自逼出了什么?当你那条约束的量级更小时,设计又会变成什么?
后半个问题才是真正值得在意的。大型金融应用会收敛到一个可辨识的形状,而这个形状常常被并不具备同样约束的团队照搬过去。下面讲的是每个组成部分背后的道理。这些判断来自把一个大型交易应用拆开、并核对理论是否与一个真实规模的团队所建造的东西吻合;随后是每个部分在什么条件下根本没有必要。
六条约束
一、新鲜度。 行情是高频、无界的数据流,价值以毫秒计地衰减。它不能被轮询,消费者永远慢于生产者,而一次被悄悄丢掉的更新不是“缺数据”,而是错数据。这逼出一条带推送、带序列号、带背压的传输,以及一个判断:哪些流可以合并,哪些流必须重新同步。 → 实时数据管线:推送、背压与合并
二、不可逆。 订单无法由用户撤销,而“恰好一次”的投递并不存在。这逼出一个在第一次发送之前就生成并落盘的幂等键、一个超时时进入的显式未知态、用对账代替盲目重试,以及一个作为唯一真相的服务端。 → 订单可靠性:幂等、未知态与对账
三、庞大的目录。 数十万只标的,要能即时搜索,且包括离线。这逼出把变化缓慢的基础数据和变化飞快的数值分开、把一份快照放到设备上、按单调版本同步增量;与此同时,账本仍然以服务端为准。 → 本地优先:快照、增量与两种状态
四、两个平台,一个真相。 iOS 和 Android 在金融规则上必须完全一致,而在交互习惯上不应该一致。这逼出:共享那些“分叉即缺陷”的层,把那些“分叉才正确”的层留给原生,并且认真对待两者之间那条边界的成本。 → 跨平台的边界:共享那些不允许分叉的部分
五、多团队,多年份。 系统必须一边改变形状,一边持续交付。这逼出:围绕“可能会变的决策”划模块边界、让核心接受被交给它的依赖而不是自己往外伸手、以及用“让新的在旧的里面长”来迁移,而不是重写。 → 模块化与迁移:划出能活下来的边界
六、不受信任的设备。 你交付出去的任何控制都可以被移除。这逼出:把客户端当作输入而永远不当作权威、安装包里不放秘密、审计实际生效而非声明的策略、并把每一个深链接和桥调用都当作敌意输入。 → 客户端安全模型:它是输入,永远不是权威
Flutter 实现篇
上面六篇与框架无关。下面这一组把同样的约束落到具体的 Flutter 实现上:给出模式、代价,以及一个你周一就能照着开工的参考方案。
- Flutter 面对高频数据:按标的分的 notifier、按帧合并,让重建范围等于变化范围
- 通道、Pigeon 与 FFI:出 Dart 的三条路、内存归属,以及
EventChannel为什么没有背压 - 能稳住 60 帧的图表:自绘、静态层与实时层分离,以及一个说实话的
shouldRepaint - 把数据库随应用一起发布:打包 SQLite、数据版本与结构版本之分、带拼音的 FTS 搜索
- 工程结构与 add-to-app:用 package 强制边界、把路由做成注册表、嵌入的真实代价
- 参考方案:行情 MVP:一个明确的计划、它刻意不建什么,以及每次升级的触发条件
判断标准
上面每一篇的结尾都写着“等测量要求时再引入”,而在有人说出这个测量是什么之前,这句话没有意义。有一篇专门给出这些数字:帧预算、消息速率、目录规模、团队人数;还给出应当先走完的“便宜修法阶梯”,以及即使过了阈值也该让你停下的反向信号。
它同时补上了系列其余部分一带而过的那个区分:正确性没有阈值。 幂等键、服务端授权、序列号、金额用十进制,在任何规模下都是第一天的工作,因为事后补它们的代价没有上界。只有性能类的决定才等一个数字。
这些约束里,哪些才是你的?
每一条需求都来自一条约束,而约束有量级。把其中一条调小,它产生的需求就会坍缩成一个小得多的东西:
| 如果你的现实是…… | 那条需求…… | 就变成…… |
|---|---|---|
| 数据以人类速度变化 | 推送、序列号、合并的管线 | 一条 WebSocket 接到 UI 状态 |
| 动作是可逆的 | 幂等键、未知态、对账 | 一个带重试的请求 |
| 目录很小或在服务端 | 设备端快照加增量同步 | 一个搜索接口 |
| 单平台,或领域逻辑很薄 | 共享跨平台核心 | 原生写一遍就好 |
| 只有一个小团队 | 模块注册表、端口与适配器 | 一份代码库,不设强制边界 |
| 低价值或公开数据 | 分层加固 | 把标准鉴权和 TLS 做扎实 |
看右边这一列。对一个做行情加自选 MVP 的小团队,同样的道理会给出:一份跨平台代码库、REST 加一条 WebSocket、一个搜索接口,不要共享核心层,不做原生性能优化,不搞自研传输。这不是大架构的简化版;对那组约束而言,它就是正确的架构,而那个大架构会是个错误。
那个大应用并不更先进。它是同样六条约束在满量级下的样子,外加多年累积的迁移中间态,而后者没有人会主动选择。
怎么用这套东西
诚实地写下你自己那六条约束的量级,逐条推出它逼你做什么,然后只造这些。
接下来,每一个重家伙都等到某条被测量到的约束真的要求它的那天才引入:队列确实在增长、性能剖析确实指向解码循环、合并冲突确实在反复发生。不要提前一个冲刺。大型架构的成本,大部分是由那些为了一个自己从未达到的规模而提前采用它的团队付掉的。
会复利的,是从约束到结构的那张映射。结构本身是一次性的;别人的结构,抄过来,是一份包装精美的负债。
参考
- D. L. Parnas,“On the Criteria To Be Used in Decomposing Systems into Modules”,CACM 15(12),1972——模块应当隐藏一个可能变化的决策。
- M. E. Conway,“How Do Committees Invent?”,Datamation,1968——系统结构会复刻构建它的组织的沟通结构。
- J. H. Saltzer, D. P. Reed, D. D. Clark,“End-to-End Arguments in System Design”,ACM TOCS 2(4),1984——保证只在能够验证它的端点上才是真的。
- M. Kleppmann,Designing Data-Intensive Applications,O’Reilly,2017——第 5 章(复制)与第 11 章(流处理)是目录同步与行情流设计的背景。
- J. Bezos,Amazon 2015 年致股东信,2016——Type 1(单向门)与 Type 2(双向门)决策。
本系列基于对一个大型交易应用的静态架构分析:存在哪些层,它们如何通信。不运行应用、不恢复源码、不涉及私有协议。详细的拆解记录被刻意排除在系列之外,因为可迁移的部分是推理过程,不是那个具体产物。