Appearance
从人的意图到系统结果:CS 世界的顶层图谱
目的
这不是一份按语言、框架或工具罗列的课程目录,而是一张帮助新人定位问题的总图:一个人的意图如何经过软件系统,最终变成可见、可共享、可恢复的结果。
学习新技术时,先把它放回这张图中,回答三个问题:它在解决哪一类问题?依赖哪些层?它的失败会传导到哪里?
总体因果链
text
人、业务目标与真实世界
|
v
交互终端表达意图
|
v
网络与协议传递命令和结果
|
v
服务判断、协调并维护共享事实
|
v
运行环境分配资源、隔离风险、处理故障
|
v
结果再次呈现给人,并进入下一轮反馈整个系统的中心不是某一个框架,而是这件事:把局部、暂时的用户操作,转化为多个参与者能够信赖的共享结果。
四个观察视角
我们最开始讨论的出发点
这张图不是先从课程目录或技术名词推出来的。它从最开始讨论时的四句原始描述出发;后续的机制树、案例和学习路径,都只是把这四句展开,不能替换它们:
- 公共 infra 基础
- client 交互终端
- server 共享事实
- 集群/沙箱/架构运行协调
它们不是严格的上下楼层,而是观察同一系统的四个窗口。遇到新技术或问题,先回到这四句,判断它主要在解决哪一类问题,再向下追机制与边界。
这四项不是严格的建筑楼层。它们从不同角度观察同一个系统,许多概念会同时出现在多个位置。
text
人、业务目标、真实世界
|
+----------------------+----------------------+
| |
交互终端层 共享事实层
Client Runtime Server Runtime
Web / Native / TUI API / 业务 / 数据 / 并发
状态、事件、呈现 权限、事务、一致性、任务
| |
+---------------- 网络与协议 ----------------+
|
运行与协调层
集群、进程、沙箱、部署、资源、故障恢复、架构
|
公共计算基础层
语言与编译、操作系统、网络、存储、密码学、算法、工程工具1. 公共计算基础层
最初描述:公共 infra 基础。
这里回答“计算、通信、存储和隔离凭什么成立”。它提供所有软件共同依赖的机制,而不等于只学习底层实现细节。
1.1 这一层真正回答的问题
- 一段程序如何变成可执行的指令,并在某个地址空间里运行?
- 多个程序如何共享 CPU、内存、磁盘和网络,同时互不破坏?
- 数据如何在本地介质和远程机器之间被可靠地表示、传输和保护?
- 协作开发时,如何让构建、测试和发布成为可重复的过程?
1.2 关键机制树
text
公共计算基础层
├── 程序表示与执行
│ ├── 语言抽象:类型、内存模型、错误处理、并发原语
│ ├── 编译 / 解释 / JIT:源码如何变成机器可执行形态
│ └── ABI 与运行时:调用约定、垃圾回收、标准库
├── 操作系统与进程模型
│ ├── 进程与线程:隔离、调度、上下文切换
│ ├── 内存:虚拟内存、页表、堆栈、共享映射
│ ├── I/O:文件、套接字、阻塞与非阻塞、事件通知
│ └── 权限与信号:用户态/内核态、能力边界、中断
├── 网络与协议
│ ├── 分层:链路、IP、传输层、应用层
│ ├── 可靠性:丢包、重传、超时、拥塞控制
│ ├── 连接语义:连接状态、半开、关闭顺序
│ └── 应用协议:HTTP、WebSocket、gRPC、消息格式
├── 存储与数据组织
│ ├── 字节与结构:编码、序列化、字节序
│ ├── 本地存储:文件系统、页缓存、持久化语义
│ ├── 索引与查询:B+ 树、哈希、顺序写与随机读
│ └── 可靠性:校验、备份、崩溃恢复的基础概念
├── 密码学与信任
│ ├── 哈希、对称/非对称加密、签名
│ ├── TLS 与证书链
│ └── 身份、密钥与最小权限的基础
└── 工程可重复性
├── 版本控制与变更历史
├── 构建系统与依赖锁定
├── 自动化测试与回归保护
└── 制品、配置与发布脚本1.3 每块机制要抓住什么
| 子模块 | 先掌握的机制 | 常见误判 |
|---|---|---|
| 语言与运行时 | 值如何存放、何时复制、错误如何传播 | 只背语法,不关心内存、生命周期和失败路径 |
| 操作系统 | 调度、隔离、I/O 模型如何决定程序行为 | 把“卡顿”一律归因于业务代码,忽略阻塞 I/O 或线程耗尽 |
| 网络 | 超时、重试、乱序、部分失败是常态 | 把一次 HTTP 调用想成本地函数调用 |
| 存储 | 顺序写、随机读、缓存层和持久化语义不同 | 以为“写入成功返回”就等于数据永远安全落盘 |
| 密码学 | 保护的是机密性、完整性还是身份 | 把“加密了”当成“系统安全了” |
| 工程工具 | 可重复构建和可验证变更 | 把本地偶然能跑通当作团队可交付 |
1.4 与上层的连接
- 交互终端依赖它提供事件循环、线程模型、字体/输入、网络栈和本地存储。
- 共享事实层依赖它提供事务、锁、网络可靠性和持久化语义的基础。
- 运行与协调层把这些能力扩展到多机、多进程和多租户场景。
新人不必先成为内核工程师,但要能在故障时判断:问题更像是语言运行时、操作系统资源、网络语义,还是存储语义。
2. 交互终端层
最初描述:client 交互终端。
Client 不等于浏览器前端。它是所有直接与人交互的运行时:Web、原生移动或桌面应用、TUI、嵌入式界面、游戏或图形工具都属于这里。
它们面对的共同结构是:
text
输入事件 / 外部消息
-> 更新本地状态
-> 发出命令或请求
-> 接收异步结果
-> 将当前状态呈现给人2.0 这一层真正回答的问题
- 用户此刻看到的是不是他正在操作的状态?
- 哪些状态只存在于本地,哪些必须以服务端事实为准?
- 输入、异步返回和生命周期事件如何汇入同一条状态更新路径?
- 呈现介质变化时,同一交互模型如何迁移?
2.0.1 机制树
text
交互终端层
├── 输入与事件
│ ├── 用户输入:点击、键盘、手势、语音
│ ├── 系统事件:前后台、旋转、窗口缩放、断连
│ └── 外部消息:推送、WebSocket、轮询结果
├── 本地状态与命令
│ ├── 视图状态:滚动位置、展开折叠、焦点
│ ├── 草稿状态:未提交表单、临时编辑
│ ├── 命令发送:请求、重试、取消、幂等键
│ └── 结果合并:乱序返回、过期响应丢弃
├── 导航与生命周期
│ ├── 页面 / 路由 / 窗口栈
│ ├── 恢复:刷新、进程被杀、会话恢复
│ └── 权限与能力:相机、通知、剪贴板
├── 呈现系统
│ ├── 结构:控件树 / DOM / 场景图 / 字符缓冲
│ ├── 布局:尺寸、位置、约束
│ ├── 绘制:文本、图像、动画、合成
│ └── 性能:增量更新、虚拟列表、图层策略
└── 体验约束
├── 无障碍、国际化、输入法
├── 弱网、离线、冲突提示
└── 可观测:前端日志、性能指标、错误上报共同问题包括状态来源、事件处理、异步结果乱序、失败恢复、导航与生命周期、输入焦点、无障碍、离线或断连,以及呈现性能。
不同终端的差异在于呈现介质和运行时约束:Web 最终操作 DOM 与浏览器能力;原生应用面对操作系统生命周期、权限和设备资源;TUI 在字符网格、键盘输入和远程终端连接内工作。它们可以共享“状态机 + 事件循环 + 呈现器”的思考方式,但不应机械复用某一个 Web 框架的实现模型。
2.1 渲染的通用骨架:从状态到可见结果
先把“渲染”从某个框架或某个浏览器 API 中抽出来。它的职责是:把终端当前能够成立的状态,投影成用户此刻能够看见、操作并理解的结果。
text
输入事件 / 外部消息 / 生命周期变化
|
v
状态转换与命令结果合并
|
v
UI 描述:此刻应该有哪些元素、内容和交互状态
|
v
更新计算:与上一帧或上一棵树相比,哪里变了
|
v
宿主呈现:布局 -> 绘制 -> 合成 / 字符输出
|
v
用户看见结果,并产生下一次输入或等待异步结果这是一条反馈回路,不是一条从上到下只执行一次的流水线。网络返回、窗口尺寸变化、系统切到后台、定时器、动画和辅助技术事件,都可能在任意时刻进入左侧,再推动下一次状态转换。
| 环节 | 它要回答什么 | 不属于它的职责 |
|---|---|---|
| 状态转换 | 这次事件后,终端的本地事实是什么 | 决定共享业务事实是否已经写成功 |
| UI 描述 | 对应这个状态,用户应该看见和能操作什么 | 直接把像素画到屏幕 |
| 更新计算 | 新旧 UI 之间哪些内容需要变 | 保证网络请求一定成功 |
| 宿主呈现 | 如何把变化变成页面、控件、图层或字符 | 替应用决定业务规则 |
| 反馈与验证 | 用户是否看见正确结果、能否继续操作 | 用视觉成功掩盖数据尚未确认 |
一个最小不变量是:**画面必须能解释当前状态,而不是只看起来像成功。**例如,点击“保存”后,画面应区分“草稿还在本地”“请求发送中”“服务端已确认”“失败可重试”;不能用同一个静态提示掩盖这些不同事实。
渲染问题也可以先按骨架定位:
| 现象 | 先检查的环节 |
|---|---|
| 数据到了但界面没变 | 状态合并、UI 描述或更新计算 |
| 文字重叠、尺寸跳动、滚动卡顿 | 布局、绘制或合成 |
| 画面已经变了但操作仍不可用 | 状态机与交互语义是否一致 |
| 用户以为保存成功,换设备却看不到 | 本地投影与共享事实的边界 |
| 输入延迟、动画掉帧 | 主线程调度、更新量或宿主呈现成本 |
后面的浏览器、原生 UI 和 TUI 都放回这条骨架中看:它们最大的差异不是“有没有渲染”,而是 UI 描述的承载物、布局绘制的宿主,以及可接受的延迟和资源约束。
关于客户端渲染的完整专题见:从浏览器内核到 Flutter。
2.2 浏览器:从文档、样式到像素
浏览器不是一个把 HTML 直接“画出来”的黑盒。以首次显示一个页面为例,关键路径大致如下:
text
HTML ----------> DOM
\
+-> 样式计算 -> 布局(Layout) -> 绘制记录(Paint)
CSS -----------> CSSOM -> 栅格化(Raster) -> 图层合成(Composite) -> 屏幕像素
JavaScript ----> 修改 DOM、样式或其他页面状态- DOM 是文档和元素的结构;CSSOM 是 CSS 规则的结构化表示。
- 浏览器根据 DOM、CSSOM、继承关系和选择器,为元素计算最终样式;不可见元素不会进入可见的渲染结果。
- 布局 计算每个盒子的大小和位置,例如一行文字会不会换行、一个弹窗处于哪里。
- 绘制 记录背景、文字、边框、阴影等应如何被画出;栅格化 将这些指令变成位图;合成 把多个图层交给 GPU 或合成器组合成最终画面。
这不是每次状态变化都必须完整重走的固定流水线。改变文字或尺寸,往往会影响样式、布局和绘制;只改动颜色可能无需重新布局;在合适条件下改动 transform 或 opacity 可以主要由合成阶段处理。性能优化的出发点不是背诵术语,而是判断一次变化究竟触发了哪些阶段。
JavaScript 并不直接“画像素”。它通过修改 DOM、样式、类名、动画状态或触发网络请求,间接驱动浏览器重新进入相关阶段。主线程如果长时间被脚本占用,输入响应、样式计算和布局都会被推迟,用户会感觉页面卡顿。
2.3 React 与浏览器管线的关系
React 处在“应用状态”和“浏览器 DOM”之间,不替代浏览器渲染引擎。
text
应用状态 / props
|
v
React 组件树(描述 UI 应该是什么)
|
v
Reconciler 比较前后描述,算出需要改动的最小集合
|
v
ReactDOM 把这些改动应用到真实 DOM
|
v
浏览器继续走样式、布局、绘制、合成可以把 React 理解成:
- 用组件函数或类描述“给定状态时 UI 应该长什么样”。
- 在状态变化时,比较新旧描述,找出插入、删除、更新节点的差异。
- 通过
ReactDOM等宿主渲染器,把差异落到真实 DOM。 - 之后的像素生成仍然完全由浏览器负责。
因此:
- React 解决的是 状态如何组织 UI 更新,不是字体排版、图层合成或 GPU 光栅化。
setState/useState触发的是 React 调度更新,不是直接重绘屏幕。key、列表协调、受控输入、并发渲染等概念,都在 DOM 更新之前的协调层。- 若组件树更新导致大量 DOM 结构或样式变化,浏览器仍可能发生昂贵的布局和绘制;React 减少的是手动 DOM 操作的复杂度,不自动消灭所有渲染成本。
一个实用判断:
| 你看到的问题 | 更可能落在哪里 |
|---|---|
| 状态改了但界面不对 | React 状态流、派生数据、错误的 props 传递 |
| 界面闪动、输入光标跳动 | DOM 更新方式、受控组件、列表 key |
| 点击后页面明显卡顿 | 组件重复渲染、过大协调,或浏览器布局/绘制过重 |
| 滚动动画掉帧 | 浏览器合成、主线程占用、图层与绘制成本 |
| 接口已返回但按钮一直转圈 | 异步结果合并、请求竞态、状态机未闭环 |
2.4 原生 App:同样的问题,不同的渲染方案
原生应用面对的问题与浏览器高度同构:输入事件进入应用,状态变化后需要更新控件树或场景图,再由系统完成布局、绘制和合成。差别在于宿主能力、生命周期和控件体系。
常见方案可以按“谁拥有渲染权”来理解:
| 方案 | 状态到像素的路径 | 典型代表 | 取舍 |
|---|---|---|---|
| 命令式原生控件 | 应用直接创建并修改系统控件;系统负责布局和绘制 | UIKit、AppKit、传统 Android View | 最贴近平台能力与系统观感;跨端复用弱,状态更新易散落在命令式代码中 |
| 声明式原生 UI | 用状态描述 UI,框架协调到原生控件树 | SwiftUI、Jetpack Compose | 保留原生渲染与可访问性优势,同时获得接近 React 的状态驱动模型 |
| 跨端桥接到原生控件 | JS/Dart 等描述 UI,桥接层映射到平台控件 | React Native | 复用 Web 思路和部分代码;桥接与平台差异会成为性能和一致性成本 |
| 自绘引擎 | 应用自带布局和绘制引擎,直接画到画布或图形层 | Flutter、部分游戏/图形框架 | 视觉与布局在多端更一致;不是使用系统控件完成每个界面,需承担引擎、包体和平台适配取舍 |
上述方案都可放回相同的抽象:
text
状态 -> UI 描述或控件树 -> 计算变化 -> 布局 -> 绘制命令 -> 合成后的画面
输入事件和异步结果 ---------------------------------------> 更新状态因此,学习浏览器渲染管线并不会把知识限制在 Web。它帮助理解的是更一般的问题:哪些状态变化应影响哪些呈现节点,框架怎样组织更新,宿主怎样把更新转换为屏幕,什么情况下这条路径会成为性能或正确性瓶颈。
2.5 TUI:把像素画面换成字符网格
TUI 省去了图形布局和 GPU 合成,但没有省去交互运行时的本质问题。它仍然维护状态,接收键盘、窗口尺寸或远程连接事件,并把当前状态投影为某个行列位置上的字符、颜色和样式。高性能 TUI 通常比较上一帧和当前帧的字符缓冲,只输出变化的终端控制序列。这与图形界面的增量更新在思想上是相通的,只是最终呈现介质变成了终端字符网格。
2.6 这一层的关键取舍
- 乐观更新可以让界面更快,但必须设计回滚和冲突提示。
- 本地缓存能提升体验,也会制造“终端已更新、共享事实未更新”的错觉。
- 全局状态并非越多越好;先分清视图状态、草稿状态和服务器状态。
- 性能优化应先定位是状态更新过频、布局过重,还是网络等待被错误表现成渲染卡顿。
3. 共享事实层
最初描述:server 共享事实。
服务端的核心不只是“提供接口”,而是决定什么可以被视为共同认可的事实。
3.1 这一层真正回答的问题
- 谁有权把一次操作写入共享世界?
- 多个参与者同时修改时,什么结果算正确?
- 超时、重试、部分成功之后,系统如何避免重复记账或丢失记账?
- 读到的数据是“此刻最新”,还是“足够新”?
3.2 机制树
text
共享事实层
├── 业务边界与模型
│ ├── 领域对象:订单、账户、计划、权限主体
│ ├── 不变量:余额不能为负、状态只能按规则迁移
│ └── 用例:创建、支付、取消、恢复、审计
├── 入口与契约
│ ├── API / RPC / 消息消费入口
│ ├── 认证:你是谁
│ ├── 鉴权:你能做什么
│ ├── 校验:输入是否合法
│ └── 版本演进:兼容、废弃、契约测试
├── 写入路径
│ ├── 事务:原子提交与回滚
│ ├── 幂等:同一命令执行一次与多次结果等价
│ ├── 并发控制:锁、版本号、CAS、队列串行化
│ └── 冲突处理:拒绝、合并、后写覆盖、人工介入
├── 读取路径
│ ├── 查询模型:详情、列表、检索、聚合
│ ├── 一致性选择:强一致读 / 最终一致读
│ └── 缓存:何处缓存、如何失效、击穿与穿透
├── 异步与集成
│ ├── 任务:生成、导出、通知、补偿
│ ├── 事件:领域事件、变更通知、订阅消费
│ └── 外部依赖:支付、模型服务、对象存储
└── 可追溯性
├── 审计日志:谁在何时因何改变了什么
├── 关联 ID:请求、任务、事务的追踪线索
└── 数据生命周期:草稿、生效、归档、删除3.3 先掌握的核心概念
| 概念 | 机制含义 | 为什么重要 |
|---|---|---|
| 不变量 | 无论系统如何失败,某些业务规则必须始终成立 | 防止“接口成功但业务语义已坏” |
| 幂等 | 同一业务命令重复到达时,不会造成重复副作用 | 网络重试和用户连点是常态 |
| 事务边界 | 哪些读写必须一起成功或一起失败 | 决定一致性成本和失败恢复方式 |
| 并发控制 | 两个写者竞争同一资源时如何裁定 | 决定超卖、脏写、丢失更新是否发生 |
| 一致性级别 | 读者何时能看见刚写入的事实 | 影响缓存、副本、跨服务设计 |
| 异步化 | 哪些工作可以延后,但仍必须可追踪 | 决定超时、用户等待和补偿流程 |
| 契约 | 客户端与服务端共同遵守的输入输出约定 | 决定系统能否独立演进 |
3.4 一条典型写入链路
text
请求进入
-> 认证与鉴权
-> 参数与业务规则校验
-> 生成或校验幂等键
-> 开启事务 / 获取必要锁或版本
-> 修改聚合根与关联数据
-> 提交事务
-> 发布事件或投递异步任务
-> 返回业务结果与可追踪标识其中最容易被低估的不是“happy path 代码”,而是:
- 第 3 步到第 6 步之间进程崩溃;
- 事件发布成功但数据库提交失败,或反过来;
- 客户端没收到响应却已经写成功,于是重试;
- 两个实例同时处理同一资源。
3.5 同步、异步与一致性的取舍
| 场景 | 更偏同步 | 更偏异步 |
|---|---|---|
| 用户必须立刻知道最终结果 | 下单扣减关键库存、权限变更即时生效 | 报告生成、视频转码、批量通知 |
| 失败必须立即回滚 | 支付确认、账户转账 | 可补偿的后续通知、搜索索引更新 |
| 跨多个不稳定依赖 | 尽量缩短同步关键路径 | 把慢依赖移出用户等待路径 |
一致性也不是口号:
- 强一致:读到的就是刚写成功的结果,成本是延迟、锁和协调。
- 最终一致:允许短暂旧读,换取吞吐和可用性,但必须处理“用户为何暂时看不见”。
- 因果一致 / 读己之写:至少保证用户刚完成的操作在自己的视图中可见。
3.6 这一层的常见误判
- 把 CRUD 接口堆砌等同于业务模型。
- 只测成功响应,不测重复提交、超时重试和并发双写。
- 用分布式事务掩盖未想清的业务边界。
- 缓存加得很快,失效和回源策略却没有主人。
- 异步任务“丢进队列”后,没有状态机、重试上限和死信处理。
4. 运行与协调层
最初描述:集群/沙箱/架构运行协调。
这一层处理程序最终在哪里运行,以及整个系统在资源有限和组件会失败的现实中如何维持服务。
4.1 这一层真正回答的问题
- 代码运行在什么隔离边界里,它能碰哪些资源?
- 实例增减、机器故障、网络分区时,服务是否仍可被发现和调用?
- 新版本如何进入生产,出错后如何快速收敛?
- 出故障时,如何从指标、日志和追踪中定位责任层?
4.2 机制树
text
运行与协调层
├── 执行与隔离
│ ├── 进程 / 容器 / 虚拟机 / 沙箱
│ ├── 资源配额:CPU、内存、磁盘、文件句柄
│ ├── 权限边界:系统调用、密钥、网络出口
│ └── 多租户隔离:噪声邻居与逃逸风险
├── 部署与拓扑
│ ├── 单机、多实例、多可用区、多区域
│ ├── 服务发现与负载均衡
│ ├── 配置与密钥分发
│ └── 流量入口:网关、Ingress、边缘节点
├── 弹性与保护
│ ├── 扩缩容与自动恢复
│ ├── 超时、重试、退避、幂等配合
│ ├── 限流、熔断、舱壁隔离
│ └── 降级:保核心路径,牺牲次要能力
├── 变更管理
│ ├── 构建制品与版本标记
│ ├── 滚动发布、蓝绿、金丝雀
│ ├── 回滚与特征开关
│ └── 迁移:配置、数据、流量的顺序
├── 可观测性
│ ├── 指标:延迟、错误率、饱和度
│ ├── 日志:结构化事件与关联字段
│ ├── 追踪:跨服务调用链
│ └── 告警与值夜:谁在何时被叫醒
└── 架构协调
├── 同步调用链 vs 事件驱动
├── 单体、模块化单体、服务化
├── 数据所有权与反腐化层
└── 成本、合规、安全基线4.3 先掌握的运行时语义
| 主题 | 关键问题 | 实践含义 |
|---|---|---|
| 隔离 | 一个组件崩溃或被攻破,会波及多大范围? | 容器、权限、网络策略不是运维装饰 |
| 发现 | 调用方如何找到健康实例? | 服务发现、健康检查、负载均衡决定可用性 |
| 超时 | 等待多久算失败? | 没有超时的重试会放大故障 |
| 重试 | 失败后是否可安全再来一次? | 必须与幂等和退避一起设计 |
| 发布 | 新旧版本并存时行为是否兼容? | 契约和数据迁移比“部署脚本成功”更关键 |
| 观测 | 用户先发现,还是系统先发现? | 没有黄金信号,故障会静默扩散 |
4.4 一次请求在这一层如何被托住
text
客户端请求
-> 入口网关 / 负载均衡
-> 鉴权与路由到某个服务实例
-> 实例内部执行业务
-> 访问数据库、缓存、消息队列或其他服务
-> 指标与追踪被记录
-> 响应返回;若实例异常,流量被切走或重试到健康实例当系统变大后,真正决定体验的常常不是业务 if/else,而是:
- 某个下游变慢后,调用方线程是否被拖死;
- 重试风暴是否把偶然故障打成全面故障;
- 配置错误是否比代码错误更快让系统不可用;
- 回滚是否真的能回到上一个已知良好状态。
4.5 架构选择的实质
架构不是画更多方框,而是决定变化与故障的边界:
- 模块化单体:先把业务边界和数据所有权理清,降低分布式复杂度。
- 服务化:在团队、发布频率和扩展需求真正出现时,再支付分布式成本。
- 事件驱动:适合解耦和异步传播,但要接受最终一致与排查复杂度。
- 平台能力下沉:把认证、限流、观测、配置做成统一能力,避免每个业务重复造轮子。
4.6 这一层的常见误判
- 把“容器跑起来了”当成“系统可运营了”。
- 只做部署,不做回滚演练和迁移顺序设计。
- 监控很多,但没有围绕用户路径定义的黄金信号。
- 在没有幂等和超时预算时大面积加重试。
- 过早微服务化,把本地函数调用变成不可靠网络调用。
同一概念会跨越多层
知识地图最终应当是图,而不是一棵只允许单一归属的树。同一个词在不同层解决不同问题;只记住技术名词,无法说明它为什么出现、失效后会怎样。
跨层概念对照
| 概念 | 公共基础层 | 交互终端层 | 共享事实层 | 运行与协调层 |
|---|---|---|---|---|
| 缓存 | 内存、页缓存、局部性 | 本地草稿、列表预取、离线包 | 查询缓存、对象缓存、只读副本 | 多实例共享缓存、失效广播、容量治理 |
| 状态 | 进程内存、寄存器、持久化介质 | 视图状态、草稿、提交中 | 业务实体、状态机、审计记录 | 部署版本、配置、集群成员状态 |
| 失败 | 系统调用错误、磁盘满、信号 | 输入被拒、请求超时、界面回滚 | 事务回滚、幂等冲突、补偿 | 实例摘除、重试、熔断、回滚发布 |
| 一致性 | 存储语义、校验、崩溃恢复 | 界面是否反映用户刚做的操作 | 多写者下的业务正确性 | 多副本、多版本并存时的收敛 |
| 安全 | 加密、密钥、最小权限原语 | 本地敏感数据、生物识别、XSS/CSRF 面 | 认证鉴权、审计、数据权限 | 网络策略、密钥分发、沙箱逃逸防护 |
| 性能 | CPU、I/O、算法复杂度 | 交互延迟、掉帧、首屏 | 查询计划、锁竞争、热点写入 | 队列堆积、饱和度、扩缩容滞后 |
| 身份 | 证书、密钥、用户与进程身份 | 登录态、会话恢复、设备绑定 | 主体、角色、资源授权 | 工作负载身份、服务间 mTLS、密钥轮换 |
| 队列 | 内核队列、缓冲、背压基础 | 用户操作排队、乐观更新队列 | 任务队列、Outbox、消费位点 | 集群队列、死信、分区与重平衡 |
阅读方式:遇到一个词,先问“它在这一层保护的是体验、事实、资源,还是物理可行性?”
一个更细的跨层例子:缓存
text
用户打开列表
-> 终端先显示本地缓存,标记“可能不是最新”
-> 请求打到服务;服务读查询缓存或只读副本
-> 若缓存未命中,回源数据库并回填
-> 若某实例写入了新数据,需要失效或版本推进
-> 集群中的其他实例不能无限期提供旧读
-> 基础层决定内存不够时谁被挤出、磁盘页缓存是否还热关键不是“用了 Redis”,而是:
- 哪个读取路径允许旧数据?
- 写入后谁负责让旧缓存失效?
- 缓存穿透、击穿、雪崩时,压力会落到哪一层?
- 用户看到旧值时,界面如何避免误操作?
业务模型:图的最上方
技术四层之下,还应有一个更上层的问题域。否则系统会变成“为了搭技术而搭技术”。
text
现实问题 / 业务目标
|
v
业务对象、规则、约束、成功标准
|
v
交互如何表达这些对象
服务如何维护这些对象的事实
运行如何保证这些对象持续可用
基础机制如何支撑以上一切业务模型要先写清的四件事
- 对象:系统里真正存在的东西是什么?例如学习计划、订单、账户、权限主体。
- 动作:谁可以对对象做什么?创建、修改、提交、取消、恢复。
- 不变量:无论怎样失败,哪些规则不能破?例如“同一订单不能重复支付成功”。
- 成功标准:对用户和业务而言,什么叫做完了?不是接口返回 200,而是结果可核对、可恢复、可审计。
没有业务模型时,四层技术很容易各自优化局部指标:界面更炫、接口更多、集群更花,却回答不了“系统到底在维护什么事实”。
本地状态与共享事实的边界
这是交互终端层与共享事实层之间最重要的契约。
| 状态类型 | 归属 | 例子 | 设计原则 |
|---|---|---|---|
| 视图状态 | 终端本地 | 滚动位置、弹窗开关、主题 | 丢了可重建,不必持久化为业务事实 |
| 草稿状态 | 终端为主,可选择性同步 | 未提交表单、编辑中的文案 | 可丢失或可恢复,但不能被当成已生效 |
| 提交中状态 | 终端投影 + 请求关联 | loading、乐观更新、重试计数 | 必须能对应到某次命令,避免无限转圈 |
| 已确认事实 | 共享事实层 | 已创建记录、已支付、已授权 | 以服务端结果为准,终端只做投影 |
| 派生视图 | 两端都可能有 | 列表汇总、未读数、进度百分比 | 要声明来源;过期时如何刷新 |
判定规则
- 只有一个用户、关掉就消失、不影响他人:多半是本地状态。
- 换设备仍要看见、多人共享、涉及钱/权限/审计:必须是共享事实。
- 终端可以乐观显示,但冲突时要有回滚或合并策略。
- “请求已发送但结果未知”应被建模为明确状态,而不是靠猜测。
text
用户点击保存
-> 终端:draft -> submitting(本地)
-> 服务:接受命令,写入事实,返回版本号
-> 终端:submitting -> confirmed,并用服务端事实替换乐观值
或 submitting -> failed,回滚并提示重试一个贯穿四层的工作例子
以“用户在任意终端创建一条记录,另一台设备能够看到”为例,把每一步映射回层级:
text
[业务] 创建一条可共享、可追溯的记录
[终端] 输入校验、显示提交中、保留草稿
[网络] 携带身份、幂等键、请求超时
[事实] 鉴权、规则校验、防重、事务写入
[运行] 实例承接请求,记录日志/追踪,失败可转移
[基础] TCP/HTTP、磁盘持久化、进程与内存保障
[终端2] 拉取或接收变更,更新列表呈现成功路径
text
用户输入
-> 客户端保存局部状态并显示提交中
-> 请求携带身份与命令到达服务
-> 服务验证规则,处理重复提交和并发冲突
-> 数据被持久化,必要时发布同步事件
-> 运行环境保证任务执行、记录日志并暴露追踪信息
-> 另一终端拉取或接收变化,更新自己的呈现失败路径清单
| 故障 | 主要落点 | 系统应有的行为 |
|---|---|---|
| 断网点击提交 | 终端 + 网络 | 保留草稿,明确“未同步”,支持稍后重试 |
| 请求超时但服务已成功 | 终端 + 事实 | 靠幂等键重试,不能新建第二条;终端最终收敛到同一事实 |
| 两台设备同时改同一记录 | 事实层并发控制 | 版本冲突、拒绝或合并,并提示用户 |
| 数据库提交成功、事件未发出 | 事实 + 异步集成 | Outbox/补偿,避免另一端永久看不到 |
| 处理实例中途崩溃 | 运行与协调 | 请求重试到健康实例,或任务重新投递且保持幂等 |
| 磁盘满 / 连接耗尽 | 公共基础 + 运行 | 快速失败、告警、保护核心路径,而不是静默写丢 |
沿这条链路提问,比按框架章节学习更快建立系统感:断网时客户端怎么做?超时重试为何必须幂等?容器重启会不会丢任务?另一台设备读到旧值是否可接受?
在 AI 加速局部实现后,人应保留的跨层判断
AI 很擅长生成某一层内部的代码,但跨层语义仍需人来守。
人要重点保留的能力
- 定义问题:系统到底要维护什么业务事实,成功标准是什么。
- 划分边界:哪些状态本地即可,哪些必须进共享事实;哪些同步,哪些异步。
- 设计失败:超时、重试、双写、乱序、部分成功时,用户和数据各看到什么。
- 选择一致性与成本:不是越强越好,而是哪条路径值得付锁和延迟的代价。
- 验证证据:用测试、日志、追踪、界面状态证明“做对了”,而不是看 AI 声称做对了。
- 控制复杂度:什么时候不该拆服务、不加缓存、不上队列。
可直接拿来审 AI 产出的问题单
- 这个改动影响的是体验、事实、资源隔离,还是基础机制?
- 重复执行一次会怎样?
- 旧版本客户端遇到新版本服务会怎样?
- 没有网络 / 下游变慢 / 进程重启时会怎样?
- 我如何用一条日志或一个测试证明它?
面向新人的分层学习路径
不要先试图学完四层。按“能解释一条链路”推进。
阶段 A:建立总图
- 目标:能画出四层,并指出任一技术大致落点。
- 练习:任选 App 里一个按钮,口述它穿过哪些层。
- 验收:不要求实现,但能区分本地显示成功和共享事实成功。
阶段 B:打通一条最小链路
- 目标:终端 -> API -> 存储 -> 再读回终端。
- 练习:创建一个记录并在另一页面/设备看到。
- 验收:补齐断网、超时、重复提交三种失败。
阶段 C:加深单层机制
- 终端:状态机、渲染更新、竞态。
- 事实:事务、幂等、权限、基本索引。
- 运行:日志、指标、超时、健康检查。
- 基础:HTTP、进程、SQL 存储语义。
- 验收:能把一个线上故障归到主责层,并说明可能波及层。
阶段 D:做跨层取舍
- 目标:在真实约束下做设计,而不是堆技术。
- 练习:给“生成报告/导出/支付/协同编辑”选同步或异步,并写清一致性与用户提示。
- 验收:能解释为什么不采用另一方案。
阶段 E:把 AI 当杠杆
- 先自己写状态机与验收标准,再让 AI 补实现。
- 让 AI 专门生成反例和测试,而不是只生成 happy path。
- 每次复盘 5 行:原假设、实际机制、跨层影响、验证方式、下次判断。
面向日常使用的记录模板
每次遇到陌生技术或故障,记四行即可:
- 落点:交互 / 共享事实 / 运行协调 / 公共基础?
- 契约:输入、输出、前提、不变量?
- 失败:用户看见什么,数据是否正确,如何恢复?
- 证据:测试、日志、追踪或界面结果如何证明?
仍待继续展开的问题
- 每层各自的“最小可掌握集合”如何进一步拆成周计划?
- 数据建模、API 设计、前端状态管理如何做成可对照的专题页?
- 如何把真实项目故障复盘持续回填到这张图的节点上?
- 安全与合规应作为贯穿属性写深,还是拆成独立观察视角?