---
url: /zh/concepts/read-models.md
description: 为什么快照是默认读模型、三个查询模型、谁能看到什么，以及怎样不让读拖慢写。
---

# 读模型

Wow 里的每一次读都从日志派生。本页说明哪些读模型不写代码就有、什么时候要自己写、谁可以看到什么，以及 Wow 怎样不让读伤到写。

## 快照是默认读模型 {#the-snapshot-is-the-default-read-model}

快照是一个聚合最新的已提交状态，加上它的元数据：版本、租户、所有者、空间、删除标记、标签、第一个和最后一个事件的时间，以及第一次和最后一次提交的操作人。同一份副本有三种用途：

| 用途 | 怎样用 |
| --- | --- |
| 更快的写 | 缓存未命中时，内核用快照加上它之后的日志条目重建状态。 |
| 查询 | 查询协议直接读快照。字段来自 Manifest 里状态的 schema，不用写投影。 |
| 视图与数据集 | 视图引擎的记录视图和分析视图默认建立在快照上。 |

内置的 `snapshot` 订阅以版本为守卫写入快照。一批里有同一聚合的多个版本时，只写最新的那个，所以写入压力随聚合数增长，而不是随提交数增长。

只有快照满足不了某种读时，才自己写投影：合并多个聚合的视图、状态里没有的形状、读必须落在某个特定存储里。

只写不查、状态又很大的聚合，可以关闭快照。

## 快照是缓存 {#snapshots-are-a-cache}

快照是派生的，不是事实。写侧从不需要重建快照：不合适的快照干脆不用，从日志重放状态即可。只有读需要快照正确，快照不正确时 Wow 会重建它们：

* **状态结构的变化是旧快照表达不了的**，例如某个演化函数的含义变了，或者新字段必须从历史计算：提高聚合的状态修订号。新部署在后台原地从日志重建已存储的快照。增加一个可选字段不需要新的修订号。
* **少数快照是错的**：用一条系统命令重建指定聚合的快照。
* **快照迁到另一个存储**：从日志开头填充新存储，追平后它成为主存储。

重建期间，查询照常回答，响应会说明数据正在重建。

## 三个查询模型 {#three-query-models}

| 模型 | 一条记录是 | 适用于 |
| --- | --- | --- |
| `snapshot` | 一个聚合的最新状态 | 基于当前状态的列表、搜索、分页、计数和聚合 |
| `events` | 一个聚合的一次提交：版本和它的事件 | 审计轨迹、时间线、按事件的分析 |
| `dataset` | 在发布的数据之上定义、由 ClickHouse 提供的宽表中的一行 | 跨聚合、跨上下文关联的分析 |

三者共用一个查询引擎：同样的过滤语言、同样的准入（schema 检查、策略、预算）、同样的观测，以及同样的关于模型能做什么的描述。每个模型带来自己的 schema 和后端。后端能做什么，比如支持哪些操作符或聚合，是客户端构建查询之前读取的能力描述的一部分，因为这取决于存储，而不是模型。

除了查询，客户端还可以直接读一个聚合的状态：最新状态、某个版本的状态、某个时间点的状态，或者状态逐次提交如何变化的轨迹。历史状态在一定限制内从日志重放得到。

## 写后即可查 {#reading-your-own-writes}

命令默认等待它的快照（[等待结果](./waits)），所以紧随其后的查询能看到这次写入。读一个聚合的状态时，还可以要求一个最小版本，即命令结果返回的那个版本：服务用日志把落后的快照追上来，必要时短暂等待；聚合仍未达到这个版本时，返回 409 `wow.query.min-version-not-reached` 并附上当前版本。聚合的第一个快照写入之前，读它的状态由日志回答。

## 谁能看到什么 {#who-sees-what}

决定调用方能读什么的事实，和决定命令能改什么的事实是同一批，承载它们的字段名在 Manifest 里只定义一次：

* **租户。** 查询总是在调用方的租户内读取。
* **所有者和空间。** 作用域里有所有者的调用方只读它拥有的聚合；租户级调用方读它租户的所有聚合。空间对两者再加以限定。声明为属于租户的聚合（例如商品）不按所有者过滤。
* **已删除的聚合**不包含在内，除非查询要求。
* **标签。** 启用基于属性的访问控制后，用调用方的属性匹配每个聚合的标签。默认关闭。
* **敏感字段**在调用方拿到的结果里被脱敏。

调用方不能看的东西，对它来说就不存在：读取它返回 404，绝不是 403，原因只写进审计日志。

## 读不能拖慢写 {#reads-must-not-slow-down-writes}

查询和命令常常共用一个数据库和它的连接池。占着连接的慢查询，会让需要连接的命令饿死。所以每个存储都有一个查询舱壁：

* **每个存储一个并发上限。** 默认由查询与写共用的连接数推出。
* **短暂排队，然后拒绝。** 超出上限的查询最多等约 200 毫秒，看是否有空出的名额。没有空出或队列已满时，以 429 `wow.query.busy` 拒绝并带上 `Retry-After`。一个页面发出的一阵查询会被吸收；持续过载则被快速拒绝。
* **跟随写入调整的上限。** 上限每秒根据写路径的健康状况自我调整：追加变慢或等待连接时减半；写入健康且有查询在等时加一。
* **可续读的流。** 流式列表在分块之间释放名额，并在持有时间到期后结束。它发出的每条记录都带一个位置，所以被服务端、网络或滚动升级断开的客户端，可以从断开的地方续读。
* **预算。** 每个查询都有时间预算。超出预算的查询以 504 `wow.query.timeout` 失败，原样重试没有意义。计数和分页总数有上限，除非允许精确计数。

ClickHouse 上的数据集经过同样的舱壁，所以分析拖不住写同一集群的 Sink。
