Cache
借助缓存优化性能,实现并发、可组合且高效的值获取。
在许多应用中,处理相互重叠的工作是很常见的。例如,在处理传入请求的服务中,避免诸如多次处理同一请求之类的重复工作就非常重要。Cache 模块通过防止重复工作来帮助提升性能。
Cache 的主要特性:
| 特性 | 说明 |
|---|---|
| 可组合性 | 允许应用的不同部分之间存在重叠工作,同时保持组合式编程。 |
| 统一的同步与异步缓存 | 通过一个统一的查找函数整合同步缓存与异步缓存,该函数能以任一方式计算值。 |
| Effect 集成 | 与 Effect 库原生协作,支持并发查找、失败处理与中断。 |
| Cache 指标 | 跟踪条目数、命中数与未命中等关键指标,为性能优化提供洞察。 |
创建 Cache
缓存由一个查找函数定义:当某个键对应的值尚未被缓存时,该函数负责计算它:
type Lookup<Key, Value, Error, Requirements> = (
key: Key,
) => Effect<Value, Error, Requirements>
查找函数接收一个 Key 并返回一个 Effect,这个 Effect 描述了如何计算对应的值(Value)。该 Effect 可能依赖某个环境(Requirements),可能以 Error 失败,也可能以 Value 成功。由于它返回的是 Effect,因此既能处理同步流程,也能处理异步流程。
创建缓存时,需要提供一个查找函数,以及缓存值的最大容量和生存时间(TTL)。
declare const make: <Key, Value, Error, Requirements>(options: {
readonly capacity: number
readonly timeToLive: Duration.DurationInput
readonly lookup: Lookup<Key, Value, Error, Requirements>
}) => Effect<Cache<Key, Value, Error>, never, Requirements>
缓存创建之后,最符合惯用法的方式是使用 get 方法。
如果缓存中已存在对应的值,get 方法会返回该值;否则它会计算一个新值,将其放入缓存并返回。
如果多个并发进程请求同一个值,该值只会被计算一次。其他所有进程会在该值可用时立即收到它。这一过程由 Effect 基于 Fiber 的并发模型管理,不会阻塞底层线程。
示例(并发的缓存查找)
在这个示例中,我们用同一个键并发地调用 timeConsumingEffect 三次。
缓存只会执行这个 effect 一次,因此并发的查找会一直等待,直到该值可用:
import { Effect, Cache, Duration } from "effect"
// Simulating an expensive lookup with a delay
const expensiveLookup = (key: string) =>
Effect.sleep("2 seconds").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("key1"), cache.get("key1"), cache.get("key1")],
{
concurrency: "unbounded",
},
)
console.log(
"Result of parallel execution of three effects" +
`with the same key: ${result}`,
)
// Fetch and display cache stats
const hits = yield* cache.cacheStats.pipe(Effect.map((stats) => stats.hits))
console.log(`Number of cache hits: ${hits}`)
const misses = yield* cache.cacheStats.pipe(
Effect.map((stats) => stats.misses),
)
console.log(`Number of cache misses: ${misses}`)
})
Effect.runPromise(program)
/*
Output:
Result of parallel execution of three effects with the same key: 4,4,4
Number of cache hits: 2
Number of cache misses: 1
*/
并发访问
缓存被设计为对并发访问安全,并且在并发场景下保持高效。如果两个并发进程请求同一个值,而该值不在缓存中,那么它只会被计算一次,并在可用时立即提供给这两个进程。并发进程会等待该值,而不会阻塞底层线程。
如果查找函数失败或被中断,错误会被传播给所有正在等待该值的并发进程。失败的结果会被缓存,以避免对同一个失败的值反复计算。如果被中断,对应的键会从缓存中移除,因此后续调用会再次尝试计算该值。
容量
创建缓存时需要指定一个容量。当缓存达到容量上限时,最近最少被访问的值会最先被移除。在两次操作之间,缓存的大小可能会略微超过指定的容量。
生存时间(TTL)
缓存还可以指定生存时间(TTL)。超过 TTL 的值不会被返回。值的存活时长从它被载入缓存时开始计算。
方法
除了 get 之外,缓存还提供了若干其他方法:
| 方法 | 说明 |
|---|---|
refresh | 触发对某个键对应值的重新计算,但不会移除旧值,从而允许继续访问。 |
size | 返回缓存当前的大小。在并发场景下,该大小是近似值。 |
contains | 检查缓存中是否存在与指定键关联的值。在并发访问下,该结果仅在检查的那一刻有效,之后可能立即发生变化。 |
invalidate | 逐出与特定键关联的值。 |
invalidateAll | 逐出缓存中的所有值。 |