Help us shape the future of Go! Take the 2026 Go Developer Survey

cache

package module
v1.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 11 Imported by: 0

README

cache

github.com/474420502/cache —— 生产可用的进程内缓存原语:后台定时/条件刷新 + 非阻塞读取 + 容量受限 LRU。

  • 纯 Go 标准库实现,零第三方依赖
  • 全部类型线程安全,泛型 API
  • 读路径无锁(一次原子加载),慢速数据源不影响读性能
  • 刷新失败保留旧值(stale-while-error),错误可见、可回调
  • 并发刷新自动合并(singleflight),防缓存击穿
  • go test -race / 96%+ 覆盖率 / 基准测试齐备

设计理念

  1. 读不阻塞:Get / Snapshot 只做一次 atomic.Pointer 加载,不抢锁、不发网络请求。
  2. 后台刷新:由唯一后台协程按 Interval 或手动触发刷新,读到的永远是"上一次刷新好的快照"。
  3. 失败保留旧值:刷新失败(含 Loader panic)时沿用上一次成功的数据,同时把错误写进快照并触发回调。
  4. 条件更新:SetParams 换数据源参数;Condition 决定本次自动刷新是否真的执行(例如只在工作时间刷新),跳过不污染成功时间。
  5. 合并并发拉取:同一时刻的多个刷新/加载请求合并成一次数据源调用。
  6. 生命周期确定:Close 幂等且可等待;Loader panic 不会打死后台协程;可选 WithAutoStop 在对象被 GC 时自动收尾。
  7. 按数据分档:WithValueCondition 决定"值处于什么区间时才继续后台刷新",WithReadPolicy 决定"每次读走缓存还是直连数据源",两者配对就能实现"安全区间缓存、敏感区间每次穿透"。

本质:定期更新数据

整个库的核心只有一件事:把数据源按时间/条件定期拉回来,发布成一个不可变快照,让读侧随时能拿到"最近一次成功的数据",且读路径永不阻塞。

AsyncCache[T] 里的 T 就是数据形态,常见三种,不需要为它们各写一个类型:

数据形态 T 读法
单个值(配置对象、版本号、开关) Config c.Get()
列表 / 切片(榜单、全量行、页面内容) []Row c.Get()
集合(按 key 查,最常用) map[K]V rows := c.Get(); rows[k]

集合形态有两条约定,守住就是无锁无撕裂:

  1. Loader 每次返回一个全新的 map(不要复用、不要原地改),旧 map 在下一次发布后自然成为垃圾;
  2. 读侧只读不写。
c := cache.NewAsync(time.Minute, func(ctx *cache.Context[map[string]User]) (map[string]User, error) {
	return db.LoadAllUsers(ctx.Ctx()) // 一次拉回整张表
})
defer c.Close()
_ = c.WaitFirst(ctx)

users := c.Get() // 一份不可变快照
u, ok := users["u1"]

这套写法由 TestAsyncCache_DatasetSnapshot(8 个读协程 + 每秒刷新,-race 下验证)和 ExampleAsyncCache_dataset 保证。什么时候不该用它:需要按 key 淘汰 / 单 key TTL / 负缓存时,说明数据不是一个整体,改用 CacheLRU(或 AsyncCache 管全量 + CacheLRU 管按 ID 查询)。

还有一种常见诉求是按数据本身分档(值在安全区间就缓存 + 周期刷新,跌进敏感区间就每次直连),见下文「值驱动的分档」。

环境

项 值
Go 版本 go 1.27.1(模块声明与本机工具链一致)
第三方依赖 无
测试 go test -race ./...
go get github.com/474420502/cache

两种缓存怎么选

AsyncCache[T] CacheLRU[K,V]
场景 定期/条件远程更新:爬虫页面、配置表、榜单、远程表数据 减少频繁操作:按 key 查询、去重、会话
数据形态 全局一份最新值 按 key 的多份值
刷新方式 后台定时/条件/手动 惰性:读时未命中才加载
读语义 永不阻塞,可能读到旧值 命中即返回;未命中会等加载
容量 不淘汰(只保留一份快照) 容量受限 + TTL

两者可以组合:用 AsyncCache 维护"热表全量",用 CacheLRU 缓存"按 ID 查询的结果"。


AsyncCache:定时/条件后台刷新

快速开始
c := cache.NewAsync(30*time.Second, func(ctx *cache.Context[[]Row]) ([]Row, error) {
	// ctx 带超时与取消,可直接传给 HTTP/DB
	return fetchRows(ctx.Ctx(), ctx.Params())
}, cache.WithTimeout(5*time.Second), cache.WithOnError(func(err error) {
	log.Println("刷新失败,继续用旧值:", err)
}))
defer c.Close()

_ = c.WaitFirst(ctx) // 可选:等首次刷新结束(成功或失败)

rows := c.Get() // 无锁读,永不阻塞;失败时是上一次成功的值
选项
选项 说明
WithInterval(d) 后台刷新间隔;<= 0 表示只做首次 + 手动刷新。也可以用 NewAsync(interval, ...) 的位置参数。固定延迟语义:下次刷新在本次结束后才计时,慢数据源不会堆积刷新
WithTimeout(d) 单次刷新超时,通过传给 Loader 的 context 实现
WithJitter(ratio) 间隔抖动比例 [0,1],避免多实例同时刷新(惊群)
WithLazyStart() 不立即刷新首次,等第一个间隔或手动触发
WithParams(p) 初始参数(SetParams 可在运行期替换)
WithCondition(f) 自动刷新前置条件(只看状态:时间/参数/失败次数),见下
WithValueCondition[T](f) 自动刷新前置条件(看当前值),见「值驱动的分档」
WithReadPolicy[T](f) Read 的读策略:走缓存还是同步穿透
WithOnError(f) 失败回调(简单版)
WithAutoStop() 对象被 GC 回收时自动关闭后台协程

也可以直接写匿名函数:NewAsync(d, loader, func(o *cache.Options) { o.Timeout = time.Second })。

Loader 与 Context
type Loader[T any] func(ctx *Context[T]) (T, error)
Context 方法 说明
Ctx() / Deadline() / Done() 本次刷新的 context,Loader 必须尊重它(否则超时和 Close 都无法及时生效)
Params() 当前参数
Current() / GetCurrent() 本次刷新开始前的值(做增量计算用)
HasCurrent() 之前是否已有成功的值
Snapshot() 本次刷新开始前的完整快照
Error() 本次刷新的错误,仅在错误回调中非 nil

Loader 在后台刷新协程中串行执行:不要在 Loader 或错误回调里调用 Close。

快照与统计
type Snapshot[T any] struct {
	Value       T
	Version     uint64    // 成功刷新次数
	Loaded      bool      // 是否至少成功过一次
	UpdatedAt   time.Time // 最近一次成功时间
	RefreshedAt time.Time // 最近一次尝试时间(失败也会更新)
	Err         error     // 最近一次刷新错误
}
  • OK():最近一次刷新成功(值是新鲜的)
  • Stale():曾经成功过,但最近一次失败(正在用旧值)
  • Stats():Refreshes / Failures / Skipped / ConditionErrors / PassThroughs / ConsecutiveFailures / LastAttempt / LastSuccess / LastError / Closed
条件更新
// 只在 09:30-15:00 之间自动刷新;首次刷新和手动 Refresh 不受限制
c := cache.NewAsync(time.Minute, loader, cache.WithCondition(func(info cache.RefreshInfo) bool {
	h, m, _ := time.Now().Clock()
	return (h == 9 && m >= 30) || (h > 9 && h < 15)
}))

Condition 返回 false 时:不调用 Loader、快照与成功时间不变、Stats().Skipped 递增。 运行期可用 SetCondition(nil) 取消限制,Condition 自身 panic 会被捕获并计入 Stats().ConditionErrors。

值驱动的分档:值大走缓存,值小每次穿透

典型场景:核心值在安全区间(比如 > 5)时按周期 N 秒后台刷新、读走缓存;一旦跌进敏感区间(<= 5),缓存就"让路"——每次读都同步直连数据源,后台也不再空拉。

c := cache.NewAsync(N*time.Second, loader,
	// ① 后台刷新的闸门:值 > 5 才周期拉取
	cache.WithValueCondition(func(s cache.Snapshot[int]) bool { return s.Value > 5 }),
	// ② 读的闸门:值 > 5 时读缓存,否则每次 Read 都同步穿透
	cache.WithReadPolicy(func(s cache.Snapshot[int]) bool { return s.Loaded && s.Value > 5 }),
)
defer c.Close()

v, err := c.Read(ctx) // 一个入口搞定两条路径
机制 作用 备注
WithValueCondition / SetValueCondition 决定后台自动刷新是否执行 与 WithCondition 逻辑与;Trigger() 也受它约束;手动 Refresh(ctx) 不受约束
WithReadPolicy / SetReadPolicy 决定每次 Read 走缓存还是同步穿透 不配置时 Read 的默认语义是"有值即返回,无值同步加载一次"
Stats().PassThroughs 穿透次数,用来观察是否长期处于敏感区间 Stats().Skipped 则是被值条件跳过的后台刷新次数

语义要点:

  • 决策依据是"快照里的当前值",不是数据源的实时值。所以"值跌破阈值"这件事,必须由某次读/刷新把新值带进快照后才会生效(这正好符合"值小就每次穿透"的用法)。
  • 两个闸门要配对使用:只设值条件不设读策略,值进入"不刷新"区间后后台就停了,缓存会停在旧值上;此时只能靠 Read(穿透)、Refresh(ctx) 或 Trigger() 把值带回来。配对后,读会持续带新值,一旦回到安全区间,后台刷新自动恢复。
  • 穿透是同步的,但同一时刻的并发穿透会合并成一次 Loader 调用(singleflight),不会把数据源打爆。
  • 穿透刷新失败时返回 (旧值, err),调用方自行决定降级用旧值还是报错;缓存已关闭时返回 (旧值, ErrClosed)。
  • 只想"每次都穿透、不要后台刷新"时更简单的写法是 NewAsync(0, loader)(首次 + 手动),或 Refresh(ctx)。

example/main.go 的 demoAdaptiveValue 完整演示了"高档位走缓存 → 跌到低档位每次穿透且后台 0 拉取 → 恢复后周期刷新自动继续"。

方法速查
方法 说明
Get() / GetWithOK() / ValueOr(def) / HasValue() 无锁读
Read(ctx) 按策略读:命中策略走缓存,否则同步穿透(并发合并)
Snapshot() / View(f) / GetSync(f) 读取快照或元信息
Stats() 运行统计
Trigger() 请求一次异步刷新(会合并、受 Condition/值条件约束),返回是否被接受
Refresh(ctx) 同步刷新,不受任何条件约束,并发调用会合并;关闭后返回 ErrClosed
ForceUpdate() Refresh(context.Background()) 的旧写法
WaitFirst(ctx) / WaitFirstUpdate(d) 等首次刷新尝试结束
WaitValue(ctx) 等第一个成功的值;一直失败则返回最后一次错误
Next(ctx) 等"下一次刷新完成",用于响应式更新本地视图
SetParams(p) / Params() 数据源参数
SetError(f) / SetErrorHandler(f) 错误回调(简单 / 带 Context)
SetCondition(f) / SetValueCondition(f) / SetReadPolicy(f) 运行期替换两个闸门与读策略
SetInterval(d) / Interval() / Timeout() / Jitter() 运行期调参
Close() / CloseWithContext(ctx) / Done() / Closed() 关闭与等待;Close 幂等
关闭与自动回收

后台协程不会自行退出,请在不再使用时 defer c.Close();Close 会等待后台协程退出(Loader 尊重 context 时是有界的),关闭后 Get 仍能读到最后一次快照。

WithAutoStop() 通过 runtime.SetFinalizer 在对象被 GC 时自动关闭协程,用于"忘记 Close"的兜底,但 finalizer 不保证在进程退出前执行,生产代码仍应显式 Close。


CacheLRU:容量受限 + TTL + 加载穿透

快速开始
lru := cache.NewCacheLRUWithTTL[int, User](1<<16, time.Minute)

lru.Set(1, u)             // 默认 TTL
lru.SetWithTTL(2, u, 0)   // 永不过期
v, ok := lru.Get(1)       // 命中会刷新 LRU 顺序
lru.Peek(1)               // 只看,不改变顺序
lru.Remove(1)
lru.Cleanup()             // 主动清理过期元素(可配合定时器)
  • 批量淘汰:容量溢出时一次性淘汰最久未用的 1/8(至少 1 个),用批量链表操作摊还成本。代价是元素个数会在 [Capacity-batch+1, Capacity] 之间波动,但永不超过容量。
  • 惰性过期:TTL 在访问时判定;长期不被访问的过期元素会占用容量直到被淘汰,需要精确回收就周期调用 Cleanup()。
  • 统计:Hits / Misses / Evictions / Expirations / Loads / LoadErrors,ResetStats() 清零。
GetOrLoad:防缓存击穿
user, err := lru.GetOrLoad(ctx, userID, func(ctx context.Context) (User, error) {
	return db.QueryUser(ctx, userID) // 同一 key 的并发请求只会执行一次
})
  • 同一 key 的并发加载合并为一次 loader 调用,结果(含 error)共享给所有等待者;
  • 等待者可以用自己的 ctx 超时退出,不会拖住共享加载;
  • loader panic 会被转成 error,不会让等待者永久阻塞;
  • loader 在锁外执行,但不要在 loader 内对同一 key 再次 GetOrLoad(会等待自己),也不要调用本缓存的其它方法;
  • 失败不写入缓存,下次调用会重试(需要负缓存时自行写入占位值 + TTL)。
分片:高并发下降锁竞争
lru := cache.NewShardedCacheLRU[int, User](1<<16, time.Minute, 16)
  • 按 key 哈希分片(hash/maphash,种子随机),同一 key 始终落在同一分片,GetOrLoad 的单飞语义不变;
  • 淘汰变成"分片内 LRU",容量平均分到各分片并向上取整,因此 Capacity() 可能略大于构造参数;
  • 分片数会向上取整到 2 的幂且不超过容量。
方法速查

Get Peek Set SetWithTTL SetIfAbsent Remove Clear Cleanup Len Size Capacity Shards Keys Values Range All GetOrLoad GetOrLoadWithTTL Stats ResetStats DefaultTTL

All() 返回标准迭代器(Go 1.23+),可以直接 range:

for key, value := range lru.All() {
	if key == target {
		break // 提前退出会正常释放锁
	}
}

Range / All 的循环体在持有分片锁时执行,里面不要再调用本缓存的其它方法。


性能(本机实测,AMD Ryzen 7 7700 / 16 线程)

go test -run XXX -bench . -benchtime=300ms .

基准 结果 说明
AsyncCache.Get(单协程) ≈ 1.0 ns/op 含一次 noinline 消费;读路径就是一次原子加载
AsyncCache.GetParallel ≈ 0.10 ns/op 16 线程并发读,近线性扩展(读路径无锁、无共享写)
AsyncCache.Snapshot ≈ 2.8 ns/op 读取完整快照结构体的拷贝成本
CacheLRU.GetOnly(单分片,并发) ≈ 89 ns/op 单锁竞争下的最坏情况
ShardedCacheLRU.GetOnly(16 分片,并发) ≈ 24 ns/op 分片后吞吐提升约 3.8 倍
CacheLRU.GetOrLoad(命中) ≈ 95 ns/op 与 Get 同价

另外,TestAsyncCache_ReadsNeverBlock 会在 Loader 阻塞期间连续读 5000 次,验证读路径不被慢数据源拖住。

生产实践建议

  1. 一定设置 WithTimeout:网络类 Loader 的兜底;否则下游挂起会让 Close 一直等待。
  2. Loader 尊重 ctx:把 ctx.Ctx() 透传给 HTTP/DB 客户端,超时与取消才能真正生效。
  3. T 视为只读:快照发布后不会再被缓存修改;如果 T 是 map/slice/指针,由 Loader 负责不并发修改(或在业务侧加锁/复制)。
  4. 错误回调要轻:它在刷新协程里同步执行,禁止阻塞、禁止调用 Close。
  5. 参数变更后主动刷新:SetParams(p) 之后调用 Trigger() 或 Refresh(ctx) 让新参数立即生效。
  6. AsyncCache 用于"全局一份最新值";按 key 的缓存请用 CacheLRU,避免自己维护 map 的并发与淘汰。

从旧版本迁移

旧 API 现状
cache.New(interval, UpdateMehtod) + Value() 改为 NewAsync(interval, Loader) + Get();支持泛型与 context
SetShare 改为 SetParams / Params
SetOnUpdateError 改为 SetError / SetErrorHandler / WithOnError
ForceUpdate() / GetUpdate() 改为 Refresh(ctx) / Snapshot().UpdatedAt
CacheBlock(惰性阻塞缓存) 用 CacheLRU.GetOrLoad 等价替代,且带 TTL 与单飞
AsyncSymbiosis / StartLoopWith 已删除(原实现是忙循环 + 阻塞式 Stop,会泄漏协程);后台循环内建,用 Close() 管理
NewAsyncCache(handler) 保留兼容,等价 NewAsync(DefaultInterval, handler, opts...);建议改用 NewAsync 明确间隔(旧实现无间隔、满速空转)
AsyncCacheContext[T] 仍是 Context[T] 的别名,GetCurrent() / Params() / Error() 保持可用

旧 CacheLRU 的批量淘汰在 Set 溢出路径上会误删刚插入的节点,并且淘汰后容量不可预期,这些问题在当前实现中已修复并有随机操作不变量测试覆盖。

目录结构

.
├── doc.go                # 包文档(设计理念 / 数据形态)
├── options.go            # AsyncCache 选项 / Condition
├── snapshot.go           # Snapshot / Stats
├── async_cache.go        # AsyncCache + 后台刷新循环 + 单飞合并
├── lru.go                # CacheLRU(分片 / TTL / GetOrLoad / 迭代器)
├── async_cache_test.go   # 定时语义用例(testing/synctest 假时钟)
├── lru_test.go           # LRU 用例(注入时钟 + 不变量 + 竞态)
├── example_test.go       # 可执行文档示例(go test 校验输出)
└── example/main.go       # 可运行的综合演示:go run ./example

测试与 Go 版本特性

特性 用在哪
testing/synctest(Go 1.24+ 稳定) 所有定时相关用例跑在假时钟里:时间只在气泡内所有协程阻塞时推进,于是"第几次刷新""抖动区间""超时耗时"都是精确断言,不用 sleep 轮询,也不会因机器负载 flaky。整套测试 < 2s。
iter.Seq2(Go 1.23+) CacheLRU.All() 支持 for k, v := range
Timer 新语义(Go 1.23+) 后台循环里 Stop/Reset 不再需要"读干"旧值
泛型类型别名 AsyncCacheContext[T] = Context[T],旧代码零改动
hash/maphash.Comparable(Go 1.24+) 分片 LRU 对任意 comparable key 做哈希

并发/竞态用例(-race)、GC/finalizer 用例、以及读路径的墙钟基准保留在真实时间与真实调度下运行。

开发
gofmt -l .                                    # 格式
go vet ./...                                  # 静态检查
go test -race -cover ./...                    # 测试 + 竞态 + 覆盖率 (96%+)
go test -race -count=5 .                      # 反复跑, 验证无 flaky
go test -run XXX -bench . -benchtime=300ms .  # 基准
go run ./example                              # 综合演示

Documentation

Overview

Package cache 提供生产可用的进程内缓存原语: 无第三方依赖, 全部类型线程安全.

设计理念

  1. 读不阻塞: 读路径只做一次原子加载, 不抢锁, 不等待网络; 慢速数据源不影响读性能.
  2. 后台刷新: 由后台协程按时间间隔或手动触发刷新, 读到的永远是"上一次刷新好的快照".
  3. 失败保留旧值: 刷新失败(含 Loader panic)时沿用上一次成功的数据, 同时把错误 写进快照 Snapshot.Err 并回调错误钩子, 即 stale-while-error.
  4. 条件更新: 通过 SetParams 改变数据源参数, 或用 Condition 决定本次是否真的拉取 (例如只在工作时间刷新), 跳过时快照保持不变, 也不会污染成功时间.
  5. 合并并发拉取: 同一时刻的多个刷新请求合并为一次 Loader 调用, 防止缓存击穿.
  6. 生命周期确定: Close 幂等且可等待; 可选 WithAutoStop 在对象被 GC 回收时自动关闭 后台协程(依赖 runtime finalizer, 不保证退出前一定执行, 推荐显式 Close).
  7. 按数据分档: WithValueCondition 决定"值在什么区间才继续后台刷新", WithReadPolicy 决定"每次 Read 走缓存还是同步穿透数据源", 两者配对即可实现 "安全区间缓存 + 周期刷新, 敏感区间每次直连".

分档用法的读入口是 Read:

c := cache.NewAsync(time.Minute, load,
	cache.WithValueCondition(func(s cache.Snapshot[int]) bool { return s.Value > 5 }),
	cache.WithReadPolicy(func(s cache.Snapshot[int]) bool { return s.Loaded && s.Value > 5 }))
v, err := c.Read(ctx) // 值大走缓存, 值小同步穿透(并发读会合并)

两种缓存

  • AsyncCache: 面向"定期/条件远程更新"的场景(爬虫页面, 配置表, 榜单, 远程表数据). 后台按 Interval 刷新, 业务侧 Get 拿到最近一次成功的值, 从不阻塞.
  • CacheLRU: 面向"减少频繁操作"的场景(按 key 查询/去重/会话). 容量受限, 支持 TTL, GetOrLoad 提供带单飞(singleflight)的加载穿透.

数据形态

库的核心只有一件事: 定期/按条件把数据源拉回来, 发布成不可变快照, 读侧永不阻塞. AsyncCache 的类型参数 T 就是数据形态:

T = Config         单个值(配置对象/版本号)
T = []Row          列表(榜单/全量行/页面内容)
T = map[K]V        集合(按 key 查, 最常用)

集合形态请让 Loader 每次返回一个全新的 map 并保证读侧只读, 这样无需任何锁:

c := cache.NewAsync(time.Minute, func(ctx *cache.Context[map[string]User]) (map[string]User, error) {
	return db.LoadAllUsers(ctx.Ctx())
})
users := c.Get() // 不可变快照
u, ok := users["u1"]

需要按 key 淘汰/单 key TTL/负缓存时, 说明数据不是一个整体, 改用 CacheLRU.

快速开始

// 每 30 秒刷新一次远程表; 读永远不阻塞
c := cache.NewAsync(30*time.Second, func(ctx *cache.Context[[]Row]) ([]Row, error) {
	return fetchRows(ctx.Ctx(), ctx.Params())
}, cache.WithTimeout(5*time.Second))
defer c.Close()

rows := c.Get() // 拿到最近一次成功的快照, 失败时是旧值

更多示例见 example/ 目录与 readme.md.

Index

Examples

Constants

View Source
const DefaultInterval = time.Minute

DefaultInterval 是 NewAsyncCache 未显式指定间隔时使用的刷新间隔.

Variables

View Source
var ErrClosed = errors.New("cache: async cache closed")

ErrClosed 表示 AsyncCache 已经关闭.

Functions

This section is empty.

Types

type AsyncCache added in v1.3.0

type AsyncCache[T any] struct {
	// contains filtered or unexported fields
}

AsyncCache 是"后台刷新 + 非阻塞读取"的快照缓存.

读接口 Get/Snapshot 只做一次原子加载, 因此永远不会被慢速数据源阻塞; 后台按 Interval 刷新, 失败时保留上一次成功的值。

请使用 NewAsync(或兼容的 NewAsyncCache) 创建实例, 并在不再使用时 Close。

Example

ExampleAsyncCache 演示后台定时刷新 + 非阻塞读取。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewAsync(time.Second, func(ctx *cache.Context[string]) (string, error) {
		return "v1", nil
	})
	defer c.Close()

	// 等首次刷新完成; 生产中也可以不等待, 直接读旧值/空值。
	_ = c.WaitFirst(context.Background())
	fmt.Println(c.Get())
	fmt.Println(c.Snapshot().Loaded)

}
Output:
v1
true
Example (Adaptive)

ExampleAsyncCache_adaptive 演示"按当前值分档": 值大走缓存, 值小每次穿透。

package main

import (
	"context"
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	source := 100

	c := cache.NewAsync(0, func(ctx *cache.Context[int]) (int, error) {
		return source, nil
	},
		cache.WithLazyStart(),
		// 值 <= 5 时后台不再周期刷新
		cache.WithValueCondition(func(s cache.Snapshot[int]) bool { return s.Value > 5 }),
		// 值 <= 5 时每次读都同步穿透
		cache.WithReadPolicy(func(s cache.Snapshot[int]) bool { return s.Loaded && s.Value > 5 }),
	)
	defer c.Close()

	v, _ := c.Read(context.Background()) // 冷启动: 无值, 穿透
	fmt.Println("首次:", v, "穿透次数:", c.Stats().PassThroughs)

	v, _ = c.Read(context.Background()) // 值 100 > 5: 走缓存
	fmt.Println("第二次:", v, "穿透次数:", c.Stats().PassThroughs)

	source = 3
	_, _ = c.Refresh(context.Background()) // 把 3 带进快照(真实场景由一次穿透完成)
	v, _ = c.Read(context.Background())    // 值 3 <= 5: 穿透
	source = 1
	v, _ = c.Read(context.Background()) // 每次都是数据源最新值
	fmt.Println("低档:", v, "穿透次数:", c.Stats().PassThroughs)

}
Output:
首次: 100 穿透次数: 1
第二次: 100 穿透次数: 1
低档: 1 穿透次数: 3
Example (Condition)

ExampleAsyncCache_condition 演示"条件更新": 只在条件满足时自动刷新。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewAsync(10*time.Millisecond, func(ctx *cache.Context[int]) (int, error) {
		return 42, nil
	}, cache.WithCondition(func(info cache.RefreshInfo) bool {
		return false // 例如: 只在交易时段刷新
	}))
	defer c.Close()

	_ = c.WaitFirst(context.Background())
	deadline := time.Now().Add(2 * time.Second)
	for c.Stats().Skipped == 0 && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
	fmt.Println(c.Get())
	fmt.Println(c.Stats().Skipped > 0)
	fmt.Println(c.Snapshot().Version) // 条件为假 -> 版本不再增长

}
Output:
42
true
1
Example (Dataset)

ExampleAsyncCache_dataset 演示"定期更新一个数据集合"的推荐形态: T = map[K]V。

Loader 每次返回一个全新的 map, 读侧只读不写, 因此天生无锁、无撕裂; 旧的 map 在下一次快照发布后自然变成垃圾。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/474420502/cache"
)

func main() {
	loadAllRows := func(ctx *cache.Context[map[string]int]) (map[string]int, error) {
		return map[string]int{"alice": 1, "bob": 2}, nil // 真实场景: 一次拉回整张表
	}

	c := cache.NewAsync(time.Minute, loadAllRows)
	defer c.Close()
	_ = c.WaitFirst(context.Background())

	rows := c.Get() // 一份不可变快照
	fmt.Println(len(rows), rows["bob"], rows["nobody"])

}
Output:
2 2 0

func NewAsync added in v1.3.0

func NewAsync[T any](interval time.Duration, loader Loader[T], opts ...Option) *AsyncCache[T]

NewAsync 创建一个后台刷新的快照缓存并立即启动刷新循环。

interval <= 0 表示不做定时刷新, 只做首次刷新与手动刷新(Trigger/Refresh)。
opts 中的 Interval 会覆盖 interval 参数。

后台协程不会自行结束, 除非调用 Close(或启用 WithAutoStop)。

func NewAsyncCache deprecated added in v1.3.0

func NewAsyncCache[T any](loader Loader[T], opts ...Option) *AsyncCache[T]

NewAsyncCache 等同于 NewAsync(DefaultInterval, loader, opts...), 用于兼容旧代码。

Deprecated: 请使用 NewAsync 明确指定刷新间隔(旧实现是"疯狂循环"刷新, 没有间隔)。

func (*AsyncCache[T]) Close added in v1.3.0

func (c *AsyncCache[T]) Close() error

Close 关闭后台刷新协程并等待其退出(幂等)。

关闭后 Get/Snapshot 仍可读到最后一次快照, Refresh 返回 ErrClosed。 若 Loader 不尊重 ctx 且未设置 Timeout, Close 会一直等待。 不要在 Loader 或错误回调里调用 Close。

func (*AsyncCache[T]) CloseWithContext added in v1.3.0

func (c *AsyncCache[T]) CloseWithContext(ctx context.Context) error

CloseWithContext 关闭后台刷新协程, 最多等待 ctx 结束。

func (*AsyncCache[T]) Closed added in v1.3.0

func (c *AsyncCache[T]) Closed() bool

Closed 报告是否已经 Close。

func (*AsyncCache[T]) Done added in v1.3.0

func (c *AsyncCache[T]) Done() <-chan struct{}

Done 返回的通道在后台刷新循环退出后关闭。

func (*AsyncCache[T]) ForceUpdate added in v1.3.0

func (c *AsyncCache[T]) ForceUpdate()

ForceUpdate 是 Refresh(context.Background()) 的旧 API 写法, 会阻塞到刷新完成。

func (*AsyncCache[T]) Get added in v1.3.0

func (c *AsyncCache[T]) Get() T

Get 返回最近一次成功刷新的值. 读路径无锁, 永不阻塞。 若从未成功刷新过, 返回 T 的零值(可用 HasValue 或 Snapshot 区分)。

func (*AsyncCache[T]) GetSync added in v1.3.0

func (c *AsyncCache[T]) GetSync(f func(current T))

GetSync 用当前值调用 f. 兼容旧 API; 值本身是只读快照, f 不会被缓存内部加锁。

func (*AsyncCache[T]) GetWithOK added in v1.3.0

func (c *AsyncCache[T]) GetWithOK() (T, bool)

GetWithOK 返回最近一次成功刷新的值以及是否有效(即是否成功刷新过)。

func (*AsyncCache[T]) HasValue added in v1.3.0

func (c *AsyncCache[T]) HasValue() bool

HasValue 报告是否至少成功刷新过一次。

func (*AsyncCache[T]) Interval added in v1.3.0

func (c *AsyncCache[T]) Interval() time.Duration

Interval 返回当前刷新间隔。

func (*AsyncCache[T]) Jitter added in v1.3.0

func (c *AsyncCache[T]) Jitter() float64

Jitter 返回当前间隔抖动比例。

func (*AsyncCache[T]) Next added in v1.3.0

func (c *AsyncCache[T]) Next(ctx context.Context) (Snapshot[T], error)

Next 返回当前快照, 并阻塞到"下一次刷新完成"(成功或失败)或 ctx 结束。 返回的第二个 error 只表示等待本身失败(ctx 结束/缓存关闭), 刷新错误在快照里。

func (*AsyncCache[T]) Params added in v1.3.0

func (c *AsyncCache[T]) Params() any

Params 返回当前参数。

func (*AsyncCache[T]) Read added in v1.4.0

func (c *AsyncCache[T]) Read(ctx context.Context) (T, error)

Read 按读策略取数, 是"按数据分档"场景的统一读入口:

  • 策略判定可以走缓存时, 直接返回当前快照值(不阻塞, 与 Get 同价);
  • 否则同步刷新一次(同一时刻的并发读会合并成一次 Loader 调用), 返回刷新后的值。

策略来自 WithReadPolicy / SetReadPolicy; 未配置时的默认语义是 "已有值就返回, 没有值就同步加载一次"(冷启动首次读不返回零值)。

刷新失败时返回 (旧值, err): 调用方可以自行决定降级使用旧值还是报错。 缓存已关闭时返回 (旧值, ErrClosed)。

策略回调在调用方协程里执行, 请保证它足够廉价且不 panic; 需要"必定同步刷新" 时不要用策略, 直接调用 Refresh。

func (*AsyncCache[T]) Refresh added in v1.3.0

func (c *AsyncCache[T]) Refresh(ctx context.Context) (Snapshot[T], error)

Refresh 同步刷新一次并返回新快照, 不受 Condition 约束, 用于"强制拉取"。

并发调用会合并为一次 Loader 调用(单飞); 等待者若 ctx 先结束则返回 ctx.Err()。 缓存已关闭时返回 ErrClosed。

func (*AsyncCache[T]) SetCondition added in v1.3.0

func (c *AsyncCache[T]) SetCondition(cond Condition)

SetCondition 设置自动刷新的前置条件; 传 nil 表示不限制。

func (*AsyncCache[T]) SetError added in v1.3.0

func (c *AsyncCache[T]) SetError(handler func(ctx *Context[T]))

SetError 设置刷新失败回调(旧 API)。回调里可用 ctx.Error() 取错误。

func (*AsyncCache[T]) SetErrorHandler added in v1.3.0

func (c *AsyncCache[T]) SetErrorHandler(handler ErrorHandler[T])

SetErrorHandler 设置刷新失败回调, 回调在刷新协程中同步执行。 回调自身 panic 会被捕获, 不会影响后台刷新。

func (*AsyncCache[T]) SetInterval added in v1.3.0

func (c *AsyncCache[T]) SetInterval(interval time.Duration)

SetInterval 动态修改刷新间隔(<=0 表示停止定时刷新, 仍可手动刷新)。

func (*AsyncCache[T]) SetParams added in v1.3.0

func (c *AsyncCache[T]) SetParams(params any)

SetParams 替换传给 Loader 的参数. 下一次刷新即可看到新参数; 需要立刻生效时再调用 Trigger 或 Refresh。

func (*AsyncCache[T]) SetReadPolicy added in v1.4.0

func (c *AsyncCache[T]) SetReadPolicy(policy ReadPolicy[T])

SetReadPolicy 设置 Read 使用的读策略, 传 nil 恢复默认("有值即返回")。

func (*AsyncCache[T]) SetValueCondition added in v1.4.0

func (c *AsyncCache[T]) SetValueCondition(cond func(s Snapshot[T]) bool)

SetValueCondition 设置"按当前值"决定自动刷新是否执行的判断, 传 nil 取消。 与 SetCondition 是逻辑与的关系; 它能看到完整快照(Unloaded/Version/Err 等)。

// 值 <= 5 时不再周期刷新(反正每次 Read 都会穿透)
c.SetValueCondition(func(s cache.Snapshot[int]) bool { return s.Value > 5 })

func (*AsyncCache[T]) Snapshot added in v1.3.0

func (c *AsyncCache[T]) Snapshot() Snapshot[T]

Snapshot 返回当前快照(值 + 版本 + 时间 + 错误)。

func (*AsyncCache[T]) Stats added in v1.3.0

func (c *AsyncCache[T]) Stats() Stats

Stats 返回运行统计。

func (*AsyncCache[T]) Timeout added in v1.3.0

func (c *AsyncCache[T]) Timeout() time.Duration

Timeout 返回当前单次刷新超时。

func (*AsyncCache[T]) Trigger added in v1.3.0

func (c *AsyncCache[T]) Trigger() bool

Trigger 请求一次异步刷新并立即返回。

返回 false 表示请求已被合并(已有待处理的刷新)或缓存已关闭。 与手动 Refresh 不同, Trigger 触发的刷新仍然受 Condition 约束。

func (*AsyncCache[T]) ValueOr added in v1.3.0

func (c *AsyncCache[T]) ValueOr(def T) T

ValueOr 在已有值(无论是否过期)时返回缓存值, 否则返回 def。

func (*AsyncCache[T]) View added in v1.3.0

func (c *AsyncCache[T]) View(f func(s Snapshot[T]))

View 用当前快照调用 f, 可以看到 Version/UpdatedAt/Err 等元信息。

func (*AsyncCache[T]) WaitFirst added in v1.3.0

func (c *AsyncCache[T]) WaitFirst(ctx context.Context) error

WaitFirst 阻塞到首次刷新"尝试"完成(成功或失败都算), 用于拿到确定性的初值。

func (*AsyncCache[T]) WaitFirstUpdate added in v1.3.0

func (c *AsyncCache[T]) WaitFirstUpdate(waitTime time.Duration) error

WaitFirstUpdate 是 WaitFirst 带超时的旧 API, 兼容旧错误文案。

func (*AsyncCache[T]) WaitValue added in v1.3.0

func (c *AsyncCache[T]) WaitValue(ctx context.Context) (T, error)

WaitValue 阻塞到出现第一个成功的值, 返回该值与 nil。 期间刷新一直失败时返回最后一次错误; ctx 结束或缓存关闭也会返回。

type AsyncCacheContext added in v1.3.0

type AsyncCacheContext[T any] = Context[T]

AsyncCacheContext 是 Context 的旧名字, 保留以兼容旧代码.

type CacheLRU added in v1.3.0

type CacheLRU[K comparable, V any] struct {
	// contains filtered or unexported fields
}

CacheLRU 是线程安全、容量受限的 LRU 缓存, 支持 TTL 与带单飞的加载穿透。

实现要点:

  • 命中时把节点移到表尾(MRU); 容量溢出时一次性淘汰表头的 1/8(至少 1 个), 用批量淘汰摊还链表操作成本, 代价是容量会在 [Capacity-batch+1, Capacity] 波动。
  • TTL 采用惰性过期: 访问时判定, 另外可用 Cleanup 主动清理(适合配合定时器)。
  • GetOrLoad 用 per-key 单飞防止缓存击穿: 同一 key 的并发加载只调用一次 loader, 共享结果; 等待者可以自己用 ctx 超时退出。
  • 内部没有后台协程, 不会因为忘记释放而泄漏。

默认构造的是单分片(严格全局 LRU)。高并发场景可以用 NewShardedCacheLRU 按 key 分片, 把锁竞争拆开: 分片后每个分片独立淘汰, 容量被平均分到各分片, 因此淘汰顺序是 "分片内 LRU" 而不是全局 LRU, 总容量也会按分片向上取整而略大于请求值。 同一 key 始终落在同一分片, 因此 GetOrLoad 的单飞语义不受影响。

Example

ExampleCacheLRU 演示基本读写与容量淘汰。

package main

import (
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewCacheLRU[int, string](4)

	for i := 1; i <= 5; i++ {
		c.Set(i, fmt.Sprintf("v%d", i))
	}

	_, ok := c.Get(1)
	fmt.Println("key 1 还在吗:", ok)
	vals := c.Values()
	fmt.Printf("最新 key 5 排第一: %s, 共 %d 个\n", vals[0], len(vals))

}
Output:
key 1 还在吗: false
最新 key 5 排第一: v5, 共 4 个

func NewCacheLRU added in v1.3.0

func NewCacheLRU[K comparable, V any](capacity int) *CacheLRU[K, V]

NewCacheLRU 创建一个容量为 capacity 的单分片 LRU 缓存, 元素永不过期。 capacity 必须大于 0, 否则 panic。

func NewCacheLRUWithTTL added in v1.3.0

func NewCacheLRUWithTTL[K comparable, V any](capacity int, ttl time.Duration) *CacheLRU[K, V]

NewCacheLRUWithTTL 创建一个容量为 capacity 的单分片 LRU 缓存, Set 写入的元素 默认在 ttl 后过期; ttl <= 0 表示不过期。可用 SetWithTTL 为单个元素指定 TTL。

func NewShardedCacheLRU added in v1.3.0

func NewShardedCacheLRU[K comparable, V any](capacity int, ttl time.Duration, shards int) *CacheLRU[K, V]

NewShardedCacheLRU 创建分片 LRU 缓存, 用于降低高并发下的锁竞争。

shards <= 1 时等价于单分片; 实际分片数会被向上取整到 2 的幂(便于位与取模), 并且不会超过 capacity。容量会平均分到各分片, 每个分片至少 1。

Example

ExampleNewShardedCacheLRU 演示分片容量分配。

package main

import (
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewShardedCacheLRU[int, string](64, 0, 4)
	defer c.Clear()
	fmt.Println("分片数:", c.Shards())
	fmt.Println("总容量:", c.Capacity())

}
Output:
分片数: 4
总容量: 64

func (*CacheLRU[K, V]) All added in v1.3.0

func (l *CacheLRU[K, V]) All() iter.Seq2[K, V]

All 返回标准迭代器, 与 range 语法配合 (Go 1.23+):

for key, value := range lru.All() {
	if key == target {
		break // 提前退出会正常释放锁
	}
}

顺序与 Range 一致; 迭代期间持有分片锁, 因此循环体内不要调用本缓存的其它方法。

Example

ExampleCacheLRU_All 演示 Go 1.23+ 的迭代器。

package main

import (
	"fmt"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewCacheLRU[int, string](8)
	c.Set(1, "one")
	c.Set(2, "two")

	for key, value := range c.All() {
		fmt.Println(key, value)
	}

}
Output:
2 two
1 one

func (*CacheLRU[K, V]) Capacity added in v1.3.0

func (l *CacheLRU[K, V]) Capacity() int

Capacity 返回容量上限. 分片时是各分片容量之和(按分片向上取整, 可能略大于构造参数)。

func (*CacheLRU[K, V]) Cleanup added in v1.3.0

func (l *CacheLRU[K, V]) Cleanup() int

Cleanup 主动删除所有已过期元素并返回删除数量, 适合配合定时器周期调用。

func (*CacheLRU[K, V]) Clear added in v1.3.0

func (l *CacheLRU[K, V]) Clear()

Clear 清空全部元素. 进行中的 GetOrLoad 不受影响, 其返回值仍会写入缓存。 统计计数不会被重置, 需要时调用 ResetStats。

func (*CacheLRU[K, V]) DefaultTTL added in v1.3.0

func (l *CacheLRU[K, V]) DefaultTTL() time.Duration

DefaultTTL 返回构造时指定的默认 TTL。

func (*CacheLRU[K, V]) Get added in v1.3.0

func (l *CacheLRU[K, V]) Get(key K) (V, bool)

Get 返回值并把该 key 标记为最近使用。已过期的元素视为不存在并被惰性删除。

func (*CacheLRU[K, V]) GetOrLoad added in v1.3.0

func (l *CacheLRU[K, V]) GetOrLoad(ctx context.Context, key K, loader func(ctx context.Context) (V, error)) (V, error)

GetOrLoad 先查缓存, 未命中时调用 loader 加载并写入缓存(TTL 取默认值)。

同一 key 的并发调用只会触发一次 loader; 等待者如果 ctx 先结束会返回 ctx.Err()。 加载失败(含 panic)不会写入缓存, 返回的 error 会共享给所有等待者。

loader 在锁外执行, 但不要在 loader 内对同一 key 再次调用 GetOrLoad(会等待自己), 也不要在 loader 内调用本缓存的其它方法。

Example

ExampleCacheLRU_GetOrLoad 演示加载穿透时的并发合并(singleflight)。

package main

import (
	"context"
	"fmt"
	"sync/atomic"
	"time"

	"github.com/474420502/cache"
)

func main() {
	c := cache.NewCacheLRUWithTTL[int, string](16, time.Minute)
	defer c.Clear()

	var queries atomic.Int64
	done := make(chan string, 8)
	for i := 0; i < 8; i++ {
		go func() {
			v, _ := c.GetOrLoad(context.Background(), 1, func(ctx context.Context) (string, error) {
				queries.Add(1)
				time.Sleep(20 * time.Millisecond)
				return "row", nil
			})
			done <- v
		}()
	}
	for i := 0; i < 8; i++ {
		<-done
	}

	fmt.Println("8 个并发只查询了", queries.Load(), "次")

}
Output:
8 个并发只查询了 1 次

func (*CacheLRU[K, V]) GetOrLoadWithTTL added in v1.3.0

func (l *CacheLRU[K, V]) GetOrLoadWithTTL(ctx context.Context, key K, ttl time.Duration, loader func(ctx context.Context) (V, error)) (V, error)

GetOrLoadWithTTL 与 GetOrLoad 相同, 但为本次加载指定 TTL。

func (*CacheLRU[K, V]) Keys added in v1.3.0

func (l *CacheLRU[K, V]) Keys() []K

Keys 返回全部未过期的 key。

单分片时按最近使用到最久未用的顺序返回; 分片时按分片拼接, 全局顺序无意义。

func (*CacheLRU[K, V]) Len added in v1.3.0

func (l *CacheLRU[K, V]) Len() int

Len 返回当前元素个数。

func (*CacheLRU[K, V]) Peek added in v1.3.0

func (l *CacheLRU[K, V]) Peek(key K) (V, bool)

Peek 与 Get 相同, 但不改变 LRU 顺序. 命中/未命中同样计入统计。

func (*CacheLRU[K, V]) Range added in v1.3.0

func (l *CacheLRU[K, V]) Range(f func(key K, value V) bool)

Range 遍历未过期元素, f 返回 false 时停止。

单分片时按最近使用到最久未用的顺序遍历; 分片时按分片遍历, 全局顺序无意义。 f 在持有分片锁的情况下执行, 因此 f 内不要调用本缓存的其它方法。

func (*CacheLRU[K, V]) Remove added in v1.3.0

func (l *CacheLRU[K, V]) Remove(key K) (V, bool)

Remove 删除元素并返回旧值与是否存在。

func (*CacheLRU[K, V]) ResetStats added in v1.3.0

func (l *CacheLRU[K, V]) ResetStats()

ResetStats 把命中/淘汰等计数清零(不影响缓存内容)。

func (*CacheLRU[K, V]) Set added in v1.3.0

func (l *CacheLRU[K, V]) Set(key K, value V)

Set 写入元素, TTL 取构造时的默认值, 并把该 key 标记为最近使用。

func (*CacheLRU[K, V]) SetIfAbsent added in v1.3.0

func (l *CacheLRU[K, V]) SetIfAbsent(key K, value V) bool

SetIfAbsent 仅在 key 不存在(或已过期)时写入, 返回是否写入成功。

func (*CacheLRU[K, V]) SetWithTTL added in v1.3.0

func (l *CacheLRU[K, V]) SetWithTTL(key K, value V, ttl time.Duration)

SetWithTTL 写入元素并指定 TTL; ttl <= 0 表示永不过期。

func (*CacheLRU[K, V]) Shards added in v1.3.0

func (l *CacheLRU[K, V]) Shards() int

Shards 返回分片数。

func (*CacheLRU[K, V]) Size added in v1.3.0

func (l *CacheLRU[K, V]) Size() int

Size 是 Len 的旧名字, 保留以兼容旧代码。

func (*CacheLRU[K, V]) Stats added in v1.3.0

func (l *CacheLRU[K, V]) Stats() LRUStats

Stats 返回累计统计。

func (*CacheLRU[K, V]) Values added in v1.3.0

func (l *CacheLRU[K, V]) Values() []V

Values 返回全部未过期的值, 顺序语义同 Keys。

type Condition added in v1.3.0

type Condition func(info RefreshInfo) bool

Condition 决定一次自动刷新是否真的执行.

type Context added in v1.3.0

type Context[T any] struct {
	// contains filtered or unexported fields
}

Context 是一次刷新的上下文. 所有方法都可以安全并发调用.

func (*Context[T]) Ctx added in v1.3.0

func (c *Context[T]) Ctx() context.Context

Ctx 返回本次刷新的 context, 可直接传给 HTTP/DB 等调用.

func (*Context[T]) Current added in v1.3.0

func (c *Context[T]) Current() T

Current 返回本次刷新开始前的值; 首次刷新(或从未成功过)时是 T 的零值.

func (*Context[T]) Deadline added in v1.3.0

func (c *Context[T]) Deadline() (time.Time, bool)

Deadline 等价于 c.Ctx().Deadline().

func (*Context[T]) Done added in v1.3.0

func (c *Context[T]) Done() <-chan struct{}

Done 等价于 c.Ctx().Done().

func (*Context[T]) Error added in v1.3.0

func (c *Context[T]) Error() error

Error 返回本次刷新的错误, 仅在错误回调中非 nil.

func (*Context[T]) GetCurrent added in v1.3.0

func (c *Context[T]) GetCurrent() T

GetCurrent 是 Current 的旧名字, 保留以兼容旧代码.

func (*Context[T]) HasCurrent added in v1.3.0

func (c *Context[T]) HasCurrent() bool

HasCurrent 报告本次刷新开始前是否已经有成功的值.

func (*Context[T]) Params added in v1.3.0

func (c *Context[T]) Params() any

Params 返回当前参数(由 SetParams / WithParams 设置).

func (*Context[T]) Snapshot added in v1.3.0

func (c *Context[T]) Snapshot() Snapshot[T]

Snapshot 返回本次刷新开始前的快照.

type ErrorHandler added in v1.3.0

type ErrorHandler[T any] func(ctx *Context[T], err error)

ErrorHandler 是刷新失败时的回调, err 同时可以通过 ctx.Error() 获取.

type LRUStats added in v1.3.0

type LRUStats struct {
	Len         int    // 当前元素个数
	Capacity    int    // 容量上限(分片时是各分片之和)
	Shards      int    // 分片数
	Hits        uint64 // Get/Peek/GetOrLoad 命中
	Misses      uint64 // 未命中(含已过期)
	Evictions   uint64 // 因容量淘汰
	Expirations uint64 // 因 TTL 过期被惰性删除
	Loads       uint64 // GetOrLoad 成功加载
	LoadErrors  uint64 // GetOrLoad 失败(含 panic)
}

LRUStats 是 CacheLRU 的累计统计(自创建或上次 ResetStats 起)。

type Loader added in v1.3.0

type Loader[T any] func(ctx *Context[T]) (T, error)

Loader 是刷新数据的函数.

ctx 携带本次刷新的上下文: Cancel/Deadline、参数 Params、上一次的值 Current 与 上一次的快照 Snapshot。返回的 error 非 nil 时不会覆盖旧值, 只会记录错误。

Loader 会在后台刷新协程中串行执行, 因此实现里不要调用 Close; 另外应当尊重 ctx 的取消与超时(WithTimeout), 否则 Close 与超时都无法及时生效。

也不要在 Loader 内调用 Refresh/ForceUpdate: 那会等待自己发起的那次刷新。

type Option added in v1.3.0

type Option func(*Options)

Option 用于配置 AsyncCache:

c := cache.NewAsync(time.Minute, loader,
	cache.WithTimeout(5*time.Second),
	cache.WithJitter(0.2),
)

也可以直接写匿名函数:

c := cache.NewAsync(time.Minute, loader, func(o *cache.Options) {
	o.Timeout = 5 * time.Second
})

func WithAutoStop added in v1.3.0

func WithAutoStop() Option

WithAutoStop 表示对象被 GC 回收时自动关闭后台协程.

func WithCondition added in v1.3.0

func WithCondition(cond Condition) Option

WithCondition 设置自动刷新的前置条件(只能看到状态, 见 Condition).

func WithInterval added in v1.3.0

func WithInterval(d time.Duration) Option

WithInterval 设置后台刷新间隔.

func WithJitter added in v1.3.0

func WithJitter(ratio float64) Option

WithJitter 设置间隔抖动比例, 取值 [0,1].

func WithLazyStart added in v1.3.0

func WithLazyStart() Option

WithLazyStart 表示构造后不立即刷新, 等第一个间隔或手动触发.

func WithOnError added in v1.3.0

func WithOnError(f func(err error)) Option

WithOnError 设置刷新失败回调.

func WithParams added in v1.3.0

func WithParams(params any) Option

WithParams 设置传给 Loader 的初始参数.

func WithReadPolicy added in v1.4.0

func WithReadPolicy[T any](policy ReadPolicy[T]) Option

WithReadPolicy 设置默认读策略, 配合 Read 使用:

// 值大于 5 时读缓存; 值 <= 5 时每次 Read 都同步穿透数据源
c := cache.NewAsync(30*time.Second, load,
	cache.WithReadPolicy(func(s cache.Snapshot[int]) bool { return s.Loaded && s.Value > 5 }))
v, err := c.Read(ctx)

不配置策略时 Read 的默认语义是"有值即返回, 无值同步加载一次"。 T 必须与 AsyncCache 的类型参数一致, 否则 NewAsync 会 panic。

func WithTimeout added in v1.3.0

func WithTimeout(d time.Duration) Option

WithTimeout 设置单次刷新的超时.

func WithValueCondition added in v1.4.0

func WithValueCondition[T any](cond func(s Snapshot[T]) bool) Option

WithValueCondition 设置"按当前值"决定自动刷新是否执行的判断, 与 WithCondition 是逻辑与的关系; 运行期可用 SetValueCondition 替换。

// 值大于 5 才周期刷新; 值 <= 5 时不再后台拉取
c := cache.NewAsync(30*time.Second, load,
	cache.WithValueCondition(func(s cache.Snapshot[int]) bool { return s.Value > 5 }))

T 必须与 AsyncCache 的类型参数一致, 否则 NewAsync 会 panic。

type Options added in v1.3.0

type Options struct {
	// Interval 是后台自动刷新的间隔. <=0 表示不做定时刷新, 只做首次刷新与手动刷新,
	// 适合"按事件/条件驱动"的场景.
	//
	// 语义是"固定延迟": 下一次刷新在本次刷新结束之后才开始计时, 因此慢数据源不会
	// 造成刷新堆积; 若一次刷新耗时超过间隔, 实际间隔就是刷新耗时。
	Interval time.Duration

	// Timeout 是单次刷新的超时时间, <=0 表示不限制.
	// 超时通过传给 Loader 的 context 实现, 因此 Loader 必须尊重 ctx.
	Timeout time.Duration

	// Jitter 是间隔抖动比例, 取值 [0,1]. 实际间隔在
	// [Interval*(1-Jitter/2), Interval*(1+Jitter/2)) 之间随机,
	// 用于避免大量实例在同一时刻集中刷新(惊群).
	Jitter float64

	// Lazy 为 true 时不立即刷新首次, 等到第一个间隔或手动触发.
	Lazy bool

	// AutoStop 为 true 时, AsyncCache 被 GC 回收时自动关闭后台协程.
	// 依赖 runtime.SetFinalizer, 不保证进程退出前一定执行, 推荐显式 Close.
	AutoStop bool

	// Params 是传给 Loader 的初始参数, 运行期可用 SetParams 替换.
	Params any

	// Condition 只在自动刷新(定时/Trigger 之外的后台刷新)时生效:
	// 返回 false 表示本次跳过, 快照与成功时间保持不变. 手动 Refresh 不受其约束.
	//
	// 它只能看到"状态"(时间/参数/失败次数); 若要按"当前值"决定是否刷新,
	// 用 WithValueCondition. 两者都设置时必须同时为真才会刷新.
	Condition Condition

	// OnError 在刷新失败(含 Loader panic)时回调. 回调在刷新协程中同步执行,
	// 不要在其中做阻塞操作, 也不要调用 Close.
	OnError func(err error)
	// contains filtered or unexported fields
}

Options 是 AsyncCache 的配置集合. 零值语义: Interval=0(不自动刷新, 仅首次+手动), Timeout=0(不限制), Jitter=0, Lazy=false(立即刷新首次), AutoStop=false, Params=nil, Condition=nil, OnError=nil.

type ReadPolicy added in v1.4.0

type ReadPolicy[T any] func(s Snapshot[T]) bool

ReadPolicy 决定一次读取是否可以由缓存直接满足: 返回 true 表示直接用当前快照(不阻塞), false 表示本次读取同步刷新数据源。

典型用法是"按当前值分档"(见 WithReadPolicy 的示例): 值在安全区间走缓存, 进入敏感区间就每次直连数据源。

type RefreshInfo added in v1.3.0

type RefreshInfo struct {
	Now                 time.Time     // 当前时间
	Params              any           // 当前参数
	Interval            time.Duration // 当前刷新间隔
	Version             uint64        // 已成功刷新次数
	Loaded              bool          // 是否已有值
	UpdatedAt           time.Time     // 上次成功时间
	RefreshedAt         time.Time     // 上次尝试时间
	Err                 error         // 上次刷新错误
	ConsecutiveFailures uint64        // 连续失败次数
}

RefreshInfo 描述一次自动刷新之前的缓存状态, 供 Condition 判断.

type Snapshot added in v1.3.0

type Snapshot[T any] struct {
	Value       T         // 最近一次成功刷新的值
	Version     uint64    // 成功刷新的次数, 从 0 开始递增
	Loaded      bool      // 是否至少成功刷新过一次(Value 是否有效)
	UpdatedAt   time.Time // 最近一次成功刷新的时间
	RefreshedAt time.Time // 最近一次尝试刷新的时间(即使失败也会更新)
	Err         error     // 最近一次刷新的错误; 成功时为 nil
}

Snapshot 是 AsyncCache 在某个时刻对外的只读视图.

快照一旦发布就不会再被修改; 若 T 是引用类型(切片/指针/map), 其内部状态由 Loader 负责保证不可变, 或者由调用方自行加锁.

func (Snapshot[T]) Age added in v1.3.0

func (s Snapshot[T]) Age(now time.Time) time.Duration

Age 返回自最近一次成功刷新以来经过的时间. 从未成功刷新过时返回 0.

func (Snapshot[T]) OK added in v1.3.0

func (s Snapshot[T]) OK() bool

OK 报告快照是否持有有效且未过期的值, 即最近一次刷新成功.

func (Snapshot[T]) Stale added in v1.3.0

func (s Snapshot[T]) Stale() bool

Stale 报告当前是否正在使用旧值: 曾经成功过, 但最近一次刷新失败了.

type Stats added in v1.3.0

type Stats struct {
	Version             uint64    // 当前成功刷新次数
	Refreshes           uint64    // 成功刷新次数
	Failures            uint64    // 失败刷新次数(含 Loader panic)
	Skipped             uint64    // 被 Condition/ValueCondition 跳过的自动刷新次数
	ConditionErrors     uint64    // Condition/ValueCondition panic 次数
	PassThroughs        uint64    // Read 因策略判定而同步穿透数据源的次数
	ConsecutiveFailures uint64    // 连续失败次数, 成功后归零
	LastAttempt         time.Time // 最近一次尝试刷新
	LastSuccess         time.Time // 最近一次成功刷新
	LastError           error     // 最近一次刷新错误
	Closed              bool      // 是否已 Close
}

Stats 是 AsyncCache 的运行统计. 字段都在一次原子读中获取, 可能存在极小的 时间偏差.

Directories

Path Synopsis
Command example 演示 cache 的典型用法。
Command example 演示 cache 的典型用法。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL