Cache
使用 Cache 优化性能,实现并发、可组合且高效的值获取。
在许多应用中,处理相互重叠的工作是很常见的。例如,在处理传入请求的 Service 中,避免重复工作(比如多次处理同一个请求)非常重要。Cache 模块通过防止重复工作来帮助提升性能。
Cache 的核心特性:
| 特性 | 说明 |
|---|---|
| 可组合性 | 允许应用中不同部分的工作相互重叠,同时保持可组合的编程方式。 |
| 统一的同步与异步 Cache | 通过统一的 lookup 函数整合同步 Cache 与异步 Cache,该函数可以按任一方式计算值。 |
| 与 Effect 集成 | 与 Effect 库原生协作,支持并发查找、失败处理和中断。 |
| Cache 检查 | 提供 Cache.size、Cache.keys 和 Cache.entries 等函数,用于检查 Cache 的当前状态。 |
创建 Cache
Cache 由一个 lookup 函数定义:当给定 key 的值尚未被缓存时,该函数负责计算这个值:
type Lookup<Key, Value, Error, Requirements> = (
key: Key,
) => Effect<Value, Error, Requirements>
lookup 函数接收一个 Key,并返回一个 Effect,这个 Effect 描述了如何计算值(Value)。该 Effect 可能需要一个环境(Requirements),可能以 Error 失败,并以 Value 成功。由于它返回的是 Effect,因此既能处理同步流程,也能处理异步流程。
创建 Cache 时,需要提供 lookup 函数,以及缓存值的最大容量和存活时间(TTL)。
declare const make: <Key, Value, Error, Requirements>(options: {
readonly capacity: number
readonly timeToLive: Duration.Input
readonly lookup: Lookup<Key, Value, Error, Requirements>
}) => Effect<Cache<Key, Value, Error>, never, Requirements>
Cache 创建之后,最符合习惯的用法是 Cache.get。
如果值已经存在,Cache.get 会返回 Cache 中的当前值;否则它会计算一个新值,将其放入 Cache,然后返回。
如果多个并发流程请求同一个值,该值只会被计算一次。其他所有流程都会在计算出的值可用时立即收到它。这由 Effect 基于 Fiber 的并发模型管理,不会阻塞底层线程。
示例(并发的 Cache 查找)
在这个示例中,我们用同一个 key 并发调用 timeConsumingEffect 三次。
Cache 只会运行这个 effect 一次,因此并发的查找会一直等到该值可用:
import { Effect, Cache, Duration } from "effect"
// Simulating an expensive lookup with a delay
const expensiveLookup = (key: string) =>
Effect.sleep("100 millis").pipe(Effect.as(key.length))
const program = Effect.gen(function* () {
// Create a cache with a capacity of 100 and an infinite TTL
const cache = yield* Cache.make({
capacity: 100,
timeToLive: Duration.infinity,
lookup: expensiveLookup,
})
// Perform concurrent lookups using the same key
const result = yield* Effect.all(
[
Cache.get(cache, "key1"),
Cache.get(cache, "key1"),
Cache.get(cache, "key1"),
],
{ concurrency: "unbounded" },
)
console.log(
"Result of parallel execution of three effects" +
`with the same key: ${result}`,
)
// The lookup only ran once, so the cache now holds a single entry
const size = yield* Cache.size(cache)
console.log(`Cache size: ${size}`)
return { result, size }
})
const output = await Effect.runPromise(program)
/*
Output:
Result of parallel execution of three effects with the same key: 4,4,4
Cache size: 1
*/
output // => { result: [4, 4, 4], size: 1 }
并发访问
Cache 被设计为对并发访问安全,并在并发条件下保持高效。如果两个并发流程请求同一个值,而它不在 Cache 中,那么这个值只会被计算一次,并在可用时立即提供给这两个流程。并发流程会等待该值,而不会阻塞底层线程。
如果 lookup 函数失败或被中断,错误会被传播给所有正在等待该值的并发流程。失败结果也会被缓存,以避免对同一个失败的值重复计算。如果被中断,该 key 会从 Cache 中移除,因此后续调用会再次尝试计算该值。
容量
创建 Cache 时会指定一个容量。当 Cache 达到容量上限时,最近最少访问的值会被优先移除。在两次操作之间,Cache 的大小可能会略微超过指定的容量。
存活时间(TTL)
Cache 还可以指定存活时间(TTL)。超过 TTL 的值不会被返回。这个时长从值被载入 Cache 的时刻开始计算。
函数
除了 Cache.get 之外,Cache 模块还提供了若干其他用于操作 Cache 的函数。每个函数都把 Cache 作为第一个参数,例如 Cache.refresh(cache, key):
| 函数 | 说明 |
|---|---|
Cache.refresh | 触发对某个 key 的值重新计算,但不会移除旧值,因此旧值仍可继续访问。 |
Cache.size | 返回 Cache 的当前大小。在并发条件下,这个大小是近似值。 |
Cache.has | 检查 Cache 中是否存在与指定 key 关联的值。在并发访问下,结果只在检查的那一刻有效,之后可能立即改变。 |
Cache.invalidate | 逐出与某个特定 key 关联的值。 |
Cache.invalidateAll | 逐出 Cache 中的所有值。 |