已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

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 中的所有值。