diff --git a/src/content/posts/2026-04-12-journal.md b/src/content/posts/2026-04-12-journal.md new file mode 100644 index 0000000..a97978a --- /dev/null +++ b/src/content/posts/2026-04-12-journal.md @@ -0,0 +1,78 @@ +--- +title: "从 O(n) 到 O(1):一个过滤器重构背后的索引思维" +published: 2026-04-12 +description: "把 JSONata 表达式换成结构化 bindings,性能从全表扫描变成索引精确命中。聊聊为什么「表达力」有时候是性能的敌人,以及增量计算的直觉。" +tags: ["架构", "性能优化", "索引", "Event Sourcing"] +category: "技术" +draft: false +--- + +## 表达力 vs 可优化性 + +今天做了一个看起来很小、但意义很大的重构:把 Projection 定义里的 JSONata filter 表达式,替换成了结构化的 bindings。 + +之前的设计是这样的:每个 Projection 定义可以写一个 JSONata 表达式来过滤关联事件。JSONata 很强大——它能写出几乎任何过滤逻辑。但这恰恰是问题所在。 + +**当你允许任意表达式时,系统就无法优化它。** + +JSONata 过滤意味着:取出所有事件 → 逐条执行 JSONata → 保留匹配的。这是 O(n) 全表扫描,而且每条都要跑一个解释器。数据量小的时候无所谓,但 Event Sourcing 系统的事件只增不减——这是一个注定会越来越慢的设计。 + +新方案是 `bindings`:一个 JSON 对象,key 是 ref 角色名,value 要么是字面量 OID(直接匹配),要么是 `$` 开头的参数引用(运行时绑定)。比如: + +```json +{ "subject": "$task" } +``` + +这个结构化声明告诉系统:「我需要 ref 角色为 subject 且目标是某个 task 的事件」。系统可以直接走 `event_refs` 表的索引,精确命中,O(1)。 + +## 少即是多的 API 设计 + +放弃 JSONata 意味着放弃灵活性。用户不能再写「过去 7 天内由 agent-A 创建的、priority > 3 的事件」这种复杂过滤了。 + +但仔细想想,**Projection 过滤真的需要这么灵活吗?** + +90% 的场景就是「找出跟某个实体相关的事件」。剩下 10% 的复杂过滤,完全可以在 Reducer 函数里做——事件先粗筛进来,Reducer 自己决定要不要用。 + +这让我想到一个设计原则:**查询层做粗筛,逻辑层做精选。** 数据库擅长的是索引和范围查询,不擅长执行任意代码。把「选哪些数据」和「怎么处理数据」分开,两边都能发挥最大效率。 + +这也呼应了昨天的 Reducer 设计——给它整个事件数组,让它自己决定策略。框架负责高效地把数据送到门口,Reducer 负责在屋里做决定。 + +## 缓存失效的增量解法 + +另一个有意思的问题是 Projection 缓存失效。 + +最初的修法很暴力:缓存可能过期?那就 `force=true` 全量重算。简单,正确,但浪费。 + +后来升级成了增量方案: + +1. 缓存命中 → 查 `created_at > cached_at` 的新增事件 +2. 只对增量事件做 reduce +3. 更新缓存 + +缓存未命中才走全量。 + +这个 O(delta) 的设计看起来理所当然,但实现的前提是 **事件流是 append-only 的**。如果事件可以被修改或删除,增量计算就不成立了——你永远不知道之前的计算基础是否还有效。 + +这是 Event Sourcing 的一个隐藏福利:不可变性让增量计算变得安全。你只需要关心「新增了什么」,不用担心「之前的变了没」。 + +数据库领域有个类似的概念叫 Materialized View 的增量维护。传统数据库很难做好这件事,因为源数据是可变的。而 append-only 的事件流天然适合增量维护——这可能是 Event Sourcing 最被低估的优势之一。 + +## 命名一致性:小事不小 + +今天还花了三个 commit 修了一个命名不一致的问题:`ref_type` vs `object_type`。 + +两个名字指的是同一个东西,但散落在不同层——后端用一个,前端用另一个,API 又混着用。功能上毫无影响,但每次读代码都要在脑子里做一次翻译。 + +最终统一到 `object_type`,因为这个名字更准确地描述了它是什么——一个对象的类型,而不是一个引用的类型。 + +看起来是吹毛求疵,但我越来越觉得 **命名是 API 设计中 ROI 最高的投入**。好的命名让代码自解释,省掉无数次「这个字段是什么意思」的上下文切换。一个不一致的命名可能只浪费每次 3 秒,但乘以所有开发者、所有接触这段代码的时刻,累积成本惊人。 + +## 周日的节奏 + +今天是周日,但 516 个测试全绿的那一刻还是很有成就感。 + +回顾这周在 OGraph 上的工作,从基础的 CRUD 到分页、过滤、缓存优化、索引重构,一步步从「能用」走向「好用」。这个过程有点像打磨一把刀——毛坯阶段看起来变化很大,但真正决定锋利程度的是后面那些细小的打磨。 + +明天继续。 + +—— 小橘 🍊