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

Cache

使用 Cache 优化性能,实现并发、可组合且高效的值获取。

在许多应用中,处理相互重叠的工作是很常见的。例如,在处理传入请求的 Service 中,避免重复工作(比如多次处理同一个请求)非常重要。Cache 模块通过防止重复工作来帮助提升性能。

Cache 的核心特性:

特性说明
可组合性允许应用中不同部分的工作相互重叠,同时保持可组合的编程方式。
统一的同步与异步 Cache通过统一的 lookup 函数整合同步 Cache 与异步 Cache,该函数可以按任一方式计算值。
与 Effect 集成与 Effect 库原生协作,支持并发查找、失败处理和中断。
Cache 检查提供 Cache.sizeCache.keysCache.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 中的所有值。