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

cache

package
v0.25.1 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package cache 进程内检查缓存原语(stdlib-only):TTL 缓存 + 刷新进行中 并发合并(inflight singleflight)+ 失败短 TTL。收编自消费方健康检查器的 双份手写形态——/health 轮询会放大昂贵的 refresh(子进程 fan-out/远程探测), 无合并则 N 个观察者并发拉起 N 轮全量刷新。

Package cache 提供进程内缓存原语与泛型缓存门面(stdlib-only,零 oekit 内部依赖)。

组成:

  • Cache[T] / CacheAside[T] / SourceFunc[T] 接口:实现契约统一收拢在 interface.go 的接口 godoc(值语义、(nil, nil) 双关、回填 best-effort、 TTL 来源、生命周期、null marker)
  • LocalCache[T]:基于内存的本地缓存(MaxSize、TTL、空值防穿透; 淘汰策略 EvictFIFO(缺省,写序)/ EvictLRU(读命中也提升顺序); NoBackgroundSweep 关闭后台清扫协程(改由调用方 Sweep 手动回收); CloneOnRead 让读命中返回副本;GetEx 区分「不存在」与「有值但值为 nil」)
  • SingleFlightCache[T]:装饰器,并发回源合并防击穿(内部 singleflight fork 于 concurrency/singleflight,见 internal/singleflight)
  • Checker:进程内检查缓存原语(TTL 缓存 + 刷新合并 + 失败短 TTL)
  • 缓存键构造:Key / HashKey / UserKey / TenantKey
  • cachetest 子包:CacheAside 实现的契约测试套件

分布式后端 RedisCache[T] 在独立模块 github.com/oekit/redisx(import github.com/oekit/redisx/cache)——本包零 Redis 编译依赖,用 LocalCache 的服务无需引入 Redis SDK。

新增后端(etcd/consul/...)实现 CacheAside[T] 后必须通过 github.com/oekit/pkg/cache/cachetest 契约测试套件(Run 全绿,含 -race) 方可接入。

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func HashKey added in v0.4.0

func HashKey(prefix string, args ...any) string

HashKey 基于参数生成 hash key,适用于复杂参数的缓存 key。 哈希取 SHA-256 前 16 位 hex(2026-09-15 治理轮:md5 → sha256,缓存 key 属非加密场景但无需为弱哈希留误用空间;摘要值较 v0.28.x 变化,key 为 进程/缓存生命周期内临时值,无持久化契约)。

cache.HashKey("search", "keyword", 1, 20) // "search:a1b2c3d4e5f6g7h8"

func Key added in v0.4.0

func Key(parts ...string) string

Key 用冒号连接各部分生成缓存 key。

cache.Key("user", "123", "profile") // "user:123:profile"

func TenantKey added in v0.4.0

func TenantKey(tenantID, suffix string) string

TenantKey 生成租户维度的缓存 key。

cache.TenantKey("tenant1", "config") // "tenant:tenant1:config"

func UserKey added in v0.4.0

func UserKey(userID int64, suffix string) string

UserKey 生成用户维度的缓存 key。

cache.UserKey(123, "profile") // "user:123:profile"

Types

type BatchGetter added in v0.6.0

type BatchGetter[T any] interface {
	// MGet 批量获取。keys 为空时返回空 map。
	MGet(ctx context.Context, keys ...string) (map[string]*T, error)
}

BatchGetter 是批量读取的可选能力接口:实现方在单次网络往返(或单次 锁区间)内取回一批键,消费方经类型断言探测选用(缓存装饰器应向下 透传:内层具备批量能力时不得在外层退化为逐键)。

实现契约(与包级 (nil, nil) 双关一致的键缺席语义):

  • 返回 map 只含命中真值的键;key 不存在与命中 null marker 一律缺席。 缺席键的消费方应回退 GetOrLoad——真 miss 回源回填,marker 命中在 后端短路,两种情况都不会重复回源;
  • MGet 是纯读:不回源、不回填、不产生 null marker;
  • 批量中的反序列化失败按整批错误处理(与单键 Get 同口径)。

type Cache added in v0.4.0

type Cache[T any] interface {
	// Get 获取缓存。
	// 返回 (nil, nil) 表示数据不存在(见包级"实现契约")。
	Get(ctx context.Context, key string) (*T, error)

	// Set 设置缓存(使用实现构造时的默认 TTL)。
	Set(ctx context.Context, key string, value *T) error

	// SetWithTTL 设置缓存(显式 TTL)。
	SetWithTTL(ctx context.Context, key string, value *T, ttl time.Duration) error

	// Del 删除缓存。key 不存在时不得报错(幂等)。
	Del(ctx context.Context, key string) error

	// DelBatch 批量删除缓存。同 Del 幂等。
	DelBatch(ctx context.Context, keys ...string) error

	// Exists 检查缓存是否存在。
	// 对 null marker 返回 true("缓存知道该 key 不存在"也是一种存在)。
	Exists(ctx context.Context, key string) (bool, error)
}

Cache 是泛型缓存接口,面向接口编程。 只定义通用的缓存操作,不绑定具体实现或模式。

实现契约(所有 Cache[T]/CacheAside[T] 实现必须遵守)

值语义(别名警告):

实现**可以**返回缓存内部共享引用(零拷贝热路径,如 LocalCache),
调用方**禁止改写**返回的 *T 指向的值——改写会直接污染缓存条目,
并与并发读构成 data race;需要可变副本时调用方自行拷贝。
序列化型实现(如 Redis 后端)每次反序列化返回新副本,天然无别名。
同一接口允许两种别名语义,因此调用方一律按"只读"对待返回值。
Set/SetWithTTL 传入的 value 同理:实现不拷贝时调用方此后不得改写。
实现可以额外提供「读命中返回副本」的可选开关(LocalCache:
LocalConfig.CloneOnRead)——开启后读命中返回副本,调用方可改写
返回值而不污染缓存条目;未开启时按上面的共享引用口径。

(nil, nil) 双关:

Get/GetOrLoad 返回 (nil, nil) 表示"数据不存在"——key miss 或命中
null marker(见 SourceFunc)二者之一;error 非 nil 时值无效。
miss 不是 error,不应记错误日志。

回填 best-effort:

GetOrLoad/Refresh 从 Source 取到数据后的回填写入失败**不阻断**返回
已取到的数据(实现自行记日志);下次请求会重新回源。

TTL 来源:

  • Set:使用实现构造时配置的默认 TTL;
  • SetWithTTL:显式传参;
  • GetOrLoad 回填:默认 TTL;实现若支持动态回填 TTL(SourceFuncWithTTL, pkg#11——如第三方 token 的 expires_in),按回源结果的 TTL 覆盖,<=0 回退默认。

TTL<=0 的语义(0 = 不过期还是实现默认值)由各实现的配置文档定义 (LocalCache:0 = 不过期;Redis 后端见各自 Config)。

生命周期:

接口不含 Close——生命周期属于具体实现的资源管理,不由接口约束。
需要停止后台资源的实现自行提供 Close,并遵循仓库硬规则签名
Close(ctx context.Context) error + sync.Once 幂等。

type CacheAside added in v0.4.0

type CacheAside[T any] interface {
	Cache[T]

	// GetOrLoad 获取缓存,未命中时通过 Source 加载并回填。
	// Source 返回 (nil, nil) 时缓存 null marker(防穿透,TTL 见 SourceFunc);
	// 后续调用命中 null marker 直接返回 (nil, nil),不再调用 Source。
	GetOrLoad(ctx context.Context, key string) (*T, error)

	// Refresh 刷新缓存(删除后重新从 Source 加载)。
	Refresh(ctx context.Context, key string) (*T, error)
}

CacheAside 是 Cache-Aside 模式扩展接口。 在 Cache 基础上增加数据源回填、刷新等能力。

GetOrLoad/Refresh 遵循包级"实现契约":miss 非 error、回填 best-effort、 null marker 防穿透。

新增后端(etcd/consul/...)必须实现本接口并通过 github.com/oekit/data/cachetest 契约测试套件(Run 全绿)方可接入。

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker 缓存一次错误型检查的结果:成功缓存 ttl,失败缓存 failTTL(失败多为 瞬时,恢复后不该被旧失败多压一整个正常周期);刷新进行中后来者合并等待同一 次刷新(ctx 感知,不叠加刷新轮次)。并发安全。

func NewChecker

func NewChecker(refresh func(ctx context.Context) error, ttl, failTTL time.Duration, opts ...Option) *Checker

NewChecker 构造;failTTL<=0 时与 ttl 同值。

func (*Checker) Check

func (c *Checker) Check(ctx context.Context) error

Check 返回缓存有效期内的结果;过期则触发一次刷新(并发调用合并为一次)。

type EvictPolicy added in v0.16.0

type EvictPolicy int

EvictPolicy 淘汰策略(LocalConfig.EvictPolicy,pkg#25)。

const (
	// EvictFIFO 写序淘汰(缺省):FIFO + 重写提升,读命中不提升。
	// 达到 MaxSize 后淘汰「最早写入且未被重写刷新」的条目。
	EvictFIFO EvictPolicy = iota

	// EvictLRU 读序淘汰:读命中同样提升顺序(最近最少使用的条目先淘汰)。
	// 锁代价:读命中需在读锁之外取写锁完成顺序提升(见 promoteOnRead 注释)。
	EvictLRU
)

type HitGetter added in v0.16.0

type HitGetter[T any] interface {
	// GetEx 获取缓存,第二个返回值为是否命中真值条目。
	// hit=false 表示未命中(不存在/已过期/null marker);hit=true 时
	// 返回值可能为 nil(命中但值为 nil)。
	GetEx(ctx context.Context, key string) (*T, bool, error)
}

HitGetter 是可选能力接口:取值时可区分「未命中」与「命中但值为 nil」。

既有 Cache[T].Get 以 (nil, nil) 同时表达「key 不存在」「命中 null marker」「命中但值为 nil」三种情况;实现本接口的方法额外给出命中 标志位(LocalCache:GetEx)。既有 Get 签名不变——本接口是可选能力, 不加入 Cache[T](避免破坏其他后端实现)。

type LocalCache added in v0.4.0

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

LocalCache 是基于内存的本地缓存实现。 适用于单实例场景或作为二级缓存。

淘汰策略由 LocalConfig.EvictPolicy 决定:

  • EvictFIFO(缺省):写序淘汰(FIFO + 重写提升,读命中不提升——非严格 LRU): 达到 MaxSize 后淘汰最早写入(且未被重写刷新)的条目;
  • EvictLRU:读命中也提升顺序,最近最少使用的条目先淘汰。

后台清理:TTL>0 时默认起一个后台清扫 goroutine,NoBackgroundSweep 置 true 可关闭它(进程内不新增任何 goroutine,过期条目由惰性删除与调用方显式 Sweep 回收)。使用 Close(ctx) 停止后台清理 goroutine;Close 可安全重复 调用,签名对齐仓库硬规则 Close(ctx context.Context) error。

func NewLocalCache added in v0.4.0

func NewLocalCache[T any](cfg LocalConfig[T]) *LocalCache[T]

NewLocalCache 创建一个本地缓存实例。

func (*LocalCache[T]) Clear added in v0.4.0

func (c *LocalCache[T]) Clear()

Clear 清空缓存。

func (*LocalCache[T]) Close added in v0.4.0

func (c *LocalCache[T]) Close(ctx context.Context) error

Close 停止后台清理 goroutine(TTL>0 且未开 NoBackgroundSweep 时构造启动的那个)。 sync.Once 保证幂等:重复 Close 不 panic、不返回错误。 无后台件时(NoBackgroundSweep=true)Close 同样安全且幂等。

ctx 预留:当前实现同步返回 nil,ctx 不阻塞——本实现无外部资源需要 排空,仅关闭内部 done channel 通知 cleanup goroutine 退出。

func (*LocalCache[T]) Del added in v0.4.0

func (c *LocalCache[T]) Del(ctx context.Context, key string) error

Del 删除缓存。

func (*LocalCache[T]) DelBatch added in v0.4.0

func (c *LocalCache[T]) DelBatch(ctx context.Context, keys ...string) error

DelBatch 批量删除缓存。

func (*LocalCache[T]) Exists added in v0.4.0

func (c *LocalCache[T]) Exists(ctx context.Context, key string) (bool, error)

Exists 检查缓存是否存在。

func (*LocalCache[T]) Get added in v0.4.0

func (c *LocalCache[T]) Get(ctx context.Context, key string) (*T, error)

Get 获取缓存。 返回 (nil, nil) 表示数据不存在 —— 包括 key 不存在和命中 null marker 两种情况;GetOrLoad 会区分二者实现防穿透。

⚠️ 返回的是缓存内部的 *T(零拷贝,热路径设计):调用方不得修改指向的 值——会污染缓存并与并发读形成 data race。需要可变副本时自行拷贝, 或配置 LocalConfig.CloneOnRead 让读命中返回副本(pkg#25)。 (RedisCache 每次反序列化返回新副本,两实现语义差异见各自身文档。)

func (*LocalCache[T]) GetEx added in v0.16.0

func (c *LocalCache[T]) GetEx(ctx context.Context, key string) (*T, bool, error)

GetEx 获取缓存并区分「不存在」与「有值但值为 nil」(pkg#25)。

返回 (value, hit, err):

  • hit=true:命中真值条目——value 可能为 nil(调用方显式存了 nil 值);
  • hit=false:key 不存在、已过期,或命中 null marker(null marker 表示 「数据不存在」,按未命中处理——与 Get 返回 (nil, nil) 同口径)。

既有 Get 签名不变(下游在用),需要区分三种「拿不到值」的情形时用本方法。

func (*LocalCache[T]) GetOrLoad added in v0.4.0

func (c *LocalCache[T]) GetOrLoad(ctx context.Context, key string) (*T, error)

GetOrLoad 获取缓存,未命中时通过 Source/SourceWithTTL 加载(Cache-Aside 模式)。 Source 返回 (nil, nil) 时缓存 null marker(默认 1 分钟,可用 NullTTL 调整,负值禁用);命中 null marker 时直接返回 (nil, nil),**不会**再调 Source,避免对不存在的 key 形成 thundering-herd 击穿。

动态回填 TTL(SourceWithTTL,pkg#11):回源返回的 ttl > 0 按其缓存, <=0 回退配置的 TTL;两者同时配置时 SourceWithTTL 优先。

stale-if-error(StaleIfError,pkg#11):回源失败且存在已过期的历史真值时 返回陈旧值并触发 OnStale 回调(fail-open,语义详见 LocalConfig 字段注释)。

读路径行为(pkg#25):

  • 命中路径与 stale 路径都算「读命中」——EvictLRU 下提升顺序, CloneOnRead 开启时返回副本;
  • 回源路径(未命中时从 Source 取到的值)不算读命中——那是调用方 自己的值,不克隆。

按调用注入回源见 GetOrLoadWithSource / GetOrLoadWithSourceTTL(pkg#77)。

func (*LocalCache[T]) GetOrLoadWithSource added in v0.24.0

func (c *LocalCache[T]) GetOrLoadWithSource(ctx context.Context, key string, source SourceFunc[T]) (*T, error)

GetOrLoadWithSource 按调用注入的 source 回源(pkg#77)。

与 GetOrLoad 的差别只有一处:回源函数由**调用方在调用点**给出,而不是 构造期固定。面向「回源依赖 key / 依赖调用上下文」的形态(token / 凭据 / 租户级缓存)——同一 key 的不同调用方可以给出各自的回源函数,不必在缓存 外再包一层 key→source 注册表。

source 为 nil 时退回构造期的 Source/SourceWithTTL(与 GetOrLoad 同口径)。 回填 TTL 取构造期默认 TTL(需要按调用指定 TTL 见 GetOrLoadWithSourceTTL)。

命中路径与 GetOrLoad 完全一致(不注入回源函数时不会被调用)。

func (*LocalCache[T]) GetOrLoadWithSourceTTL added in v0.24.0

func (c *LocalCache[T]) GetOrLoadWithSourceTTL(ctx context.Context, key string, source SourceFuncWithTTL[T], ttl time.Duration) (*T, error)

GetOrLoadWithSourceTTL 按调用注入的 source 回源并按 ttl 回填(pkg#77)。

与 GetOrLoadWithSource 的差别:回源函数可携带动态 TTL(SourceFuncWithTTL), 且 ttl > 0 时**覆盖**回源给出的 TTL(ttl <= 0 时按回源给出的 TTL, 仍无则退回构造期默认 TTL)。

source 为 nil 时退回构造期的 Source/SourceWithTTL;此时 ttl > 0 仍生效 (覆盖构造期回源给出的 TTL)。

典型场景:第三方 token 的 expires_in 随租户而异——回源函数按 key 取凭据、 动态 TTL 与调用点注入的凭据来源一并给出。

func (*LocalCache[T]) MGet added in v0.6.0

func (c *LocalCache[T]) MGet(ctx context.Context, keys ...string) (map[string]*T, error)

MGet 批量获取:单次读锁区间取回一批键(契约见 BatchGetter——缺席语义 与 (nil, nil) 双关一致:不存在与 null marker 一律缺席;纯读不回填, 已过期条目按缺席处理,回收留给 Get/Exists 的双检路径——TTL=0 时无 cleanup goroutine,过期条目仅在这些路径被惰性回收)。

CloneOnRead 开启时按配置返回副本(pkg#25);EvictLRU 策略下批量命中同样 提升顺序(读锁区间之外一次性提升,避免逐键取写锁)。

func (*LocalCache[T]) Peek added in v0.23.0

func (c *LocalCache[T]) Peek(ctx context.Context, key string) (*T, bool, error)

Peek 非删除式读取(pkg#72):读到过期条目时**不删除**它,只是不命中。

与 Get 的差别:Get 走 getEntry 的双检路径,命中过期条目时会惰性删除 (回收过期条目);Peek 只看不删——过期真值留在表内,stale-if-error 兜底 (回源失败时回退历史真值)才有值可退。装饰器的前置检查必须走本方法: 走 Get 会提前删掉 stale 兜底需要的那份值,使 StaleIfError 形同虚设。

命中语义与 GetEx 一致:

  • hit=true:命中真值条目——value 可能为 nil(调用方显式存了 nil 值);
  • hit=false:key 不存在、已过期,或命中 null marker。

命中真值条目时行为与 Get 一致(LRU 顺序提升 + CloneOnRead 返回副本)。

func (*LocalCache[T]) Refresh added in v0.4.0

func (c *LocalCache[T]) Refresh(ctx context.Context, key string) (*T, error)

Refresh 刷新缓存(删除后重新从 Source 加载)。

func (*LocalCache[T]) Set added in v0.4.0

func (c *LocalCache[T]) Set(ctx context.Context, key string, value *T) error

Set 设置缓存(使用默认 TTL)。

func (*LocalCache[T]) SetWithTTL added in v0.4.0

func (c *LocalCache[T]) SetWithTTL(ctx context.Context, key string, value *T, ttl time.Duration) error

SetWithTTL 设置缓存(自定义 TTL)。

func (*LocalCache[T]) Size added in v0.4.0

func (c *LocalCache[T]) Size() int

Size 返回当前缓存条目数。

func (*LocalCache[T]) Stats added in v0.24.0

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

Stats 返回当前观测计数快照(pkg#76)。

口径见 cache.Stats:Hits/Misses 是读路径的命中与未命中计数,Evictions 是 容量淘汰次数,Expirations 是 TTL 过期回收次数,Size 是条目数。 各计数单调不减;快照为近似一致(并发下各字段可能来自略有差别的时刻)。

func (*LocalCache[T]) Sweep added in v0.16.0

func (c *LocalCache[T]) Sweep(ctx context.Context) int

Sweep 手动清扫过期条目,返回本次清除的条目数(pkg#25)。

供「不起后台清扫协程」模式(LocalConfig.NoBackgroundSweep)下由调用方 在合适时机回收过期条目——短命 CLI/宿主内嵌场景不必常驻清扫协程。 过期判定走注入时钟(LocalConfig.Now),与后台清扫同一口径。

ctx 已取消时直接返回 0(不执行清扫)。可与后台清扫并存——两者各自 独立判定,重复清扫不会重复计数(已被回收的条目不在表内)。

type LocalConfig added in v0.4.0

type LocalConfig[T any] struct {
	// TTL 缓存过期时间。0 表示不过期。
	TTL time.Duration

	// MaxSize 最大缓存条目数。0 表示不限制。
	MaxSize int

	// Source 缓存未命中时的数据获取函数(Cache-Aside 模式)。
	Source SourceFunc[T]

	// NullTTL 空值缓存过期时间(防穿透)。
	//   - 0(未设置):使用默认值 1 分钟;
	//   - 负值:显式禁用空值缓存(Source 返回 nil 时不写 null marker,
	//     下次 GetOrLoad 会重新调用 Source);
	//   - 正值:按指定时长缓存 null marker。
	NullTTL time.Duration

	// Now 时钟注入(缺省 time.Now;nil 视为缺省)。TTL/NullTTL 的全部过期判定
	// 与过期时刻写入走该时钟——测试注入假时钟即可确定性驱动过期/空值窗口,
	// 无 sleep 无 flake(与 memkv 的 s.now、budget 的 WithClock 口径拉齐,#9)。
	Now func() time.Time

	// SourceWithTTL 动态回填 TTL 的回源函数(pkg#11;与 Source 同时配置时本字段
	// 优先):ttl > 0 按其缓存,<=0 回退 TTL 字段;返回 nil 值仍走 null marker
	// (此时 ttl 不生效,NullTTL 治理)。典型场景:第三方 token 的 expires_in。
	SourceWithTTL SourceFuncWithTTL[T]

	// StaleIfError 回源失败时回退已过期的历史真值(fail-open,pkg#11——防上游
	// 抖动放大为拒绝服务)。只认「TTL 过期的真值」:null marker 不参与 stale,
	// TTL=0 永不过期条目无 stale 语义(不会过期)。过期条目在本路径下保留于表内
	// (回收交给后台清扫),可反复兜底直至上游恢复。
	StaleIfError bool

	// OnStale stale 命中回调(观测:打点/告警;nil = no-op)。
	// value 所有权与缓存值一致(共享引用、调用方只读)。
	OnStale func(ctx context.Context, key string, stale *T)

	// NoBackgroundSweep 关闭后台清扫 goroutine(pkg#25):置 true 时
	// NewLocalCache 之后进程内不新增任何 goroutine——短命命令行/宿主内嵌
	// 场景不该为缓存常驻一个清扫协程。此时过期条目由惰性删除(Get/Exists
	// 读路径)与调用方显式调用 Sweep 回收。Close 仍安全且幂等。
	NoBackgroundSweep bool

	// CloneOnRead 读命中返回副本(pkg#25;nil = 保持零拷贝)。
	// 开启后 Get/GetOrLoad(命中与 stale 路径)/MGet 返回的是副本,调用方
	// 改写返回值不会污染缓存里的那份。克隆函数返回 nil 时视为不克隆
	// (返回原值)。回源路径(GetOrLoad 未命中时从 Source 取到的值)不克隆
	// ——那是调用方自己的值。OnStale 回调同样不克隆(回调值所有权与缓存值一致)。
	CloneOnRead func(*T) *T

	// EvictPolicy 淘汰策略(pkg#25;缺省 EvictFIFO)。
	//   - EvictFIFO:写序淘汰(FIFO + 重写提升,读命中不提升)——历史行为;
	//   - EvictLRU:读命中也提升顺序(最近最少使用先淘汰)。
	EvictPolicy EvictPolicy
}

LocalConfig 配置本地缓存实例。

type Option

type Option func(*Checker)

Option Checker 可选项。

func WithClock

func WithClock(now func() time.Time) Option

WithClock 注入时钟(测试用;缺省 time.Now)。

type PeekGetter added in v0.23.0

type PeekGetter[T any] interface {
	// Peek 非删除式获取缓存,第二个返回值为是否命中真值条目。
	// 命中真值条目时行为与 Get 一致(含读命中顺序提升与 CloneOnRead);
	// 过期条目只算不命中,且**留在表内不被删除**。
	Peek(ctx context.Context, key string) (*T, bool, error)
}

PeekGetter 是可选能力接口:非删除式读取(LocalCache 实现:Peek)。

与 Get 的唯一差别是**读到过期条目时不删除它**:Get 走惰性删除路径 (读路径顺带回收过期条目),而 stale-if-error 兜底(回源失败回退已过期 的历史真值)需要那份值仍留在表内。装饰器的前置检查若走 Get,会提前把 它删掉,使 StaleIfError 形同虚设(pkg#72)。

命中语义与 HitGetter 一致:hit=false 表示未命中(不存在/已过期/null marker);hit=true 时返回值可能为 nil(命中但值为 nil)。

type SingleFlightCache added in v0.4.0

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

SingleFlightCache 是带 singleflight 保护的缓存装饰器。 同一个 key 的并发请求只会有一个实际调用 Source,其他请求等待共享结果。 防止缓存击穿(热点 key 过期时的雷群效应)。

可以包装任何 CacheAside[T] 实现(Redis、本地缓存等)。

func NewSingleFlightCache added in v0.4.0

func NewSingleFlightCache[T any](inner CacheAside[T]) *SingleFlightCache[T]

NewSingleFlightCache 创建带 singleflight 保护的缓存装饰器。 inner 可以是 RedisCache、LocalCache 或任何实现 CacheAside[T] 的缓存。

func (*SingleFlightCache[T]) Del added in v0.4.0

func (sf *SingleFlightCache[T]) Del(ctx context.Context, key string) error

Del 删除缓存。

func (*SingleFlightCache[T]) DelBatch added in v0.4.0

func (sf *SingleFlightCache[T]) DelBatch(ctx context.Context, keys ...string) error

DelBatch 批量删除缓存。

func (*SingleFlightCache[T]) Exists added in v0.4.0

func (sf *SingleFlightCache[T]) Exists(ctx context.Context, key string) (bool, error)

Exists 检查缓存是否存在。

func (*SingleFlightCache[T]) Get added in v0.4.0

func (sf *SingleFlightCache[T]) Get(ctx context.Context, key string) (*T, error)

Get 获取缓存(纯查询,不触发 Source)。

func (*SingleFlightCache[T]) GetOrLoad added in v0.4.0

func (sf *SingleFlightCache[T]) GetOrLoad(ctx context.Context, key string) (*T, error)

GetOrLoad 获取缓存,带 singleflight 保护。 同一个 key 的并发请求只会有一个实际调用 Source。

流程:

  1. 查缓存,命中则直接返回
  2. 未命中时通过 singleflight 去重,只有一个 goroutine 调用 Source
  3. Source 返回后回填缓存
  4. 其他 goroutine 共享同一份结果

⚠️ 前置检查走**非删除式**读路径(Peek,pkg#72):走 Get 会命中 LocalCache 的惰性删除路径,把 stale-if-error 兜底需要的过期真值删掉 ⇒ 包装 LocalCache 后 StaleIfError 形同虚设(回源失败直接抛错,不再兜底历史真值)。

func (*SingleFlightCache[T]) GetOrLoadWithSource added in v0.24.0

func (sf *SingleFlightCache[T]) GetOrLoadWithSource(ctx context.Context, key string, source SourceFunc[T]) (*T, error)

GetOrLoadWithSource 按调用注入的 source 回源(pkg#77,带 singleflight 保护)。

与 GetOrLoad 的差别只有一处:回源函数由**调用方在调用点**给出,而不是 构造期固定——面向「回源依赖 key / 依赖调用上下文」的形态(token / 凭据 / 租户级缓存)。单飞语义不变:同一 key 的并发请求只会有一个实际调用 source。

内层不支持按调用注入(未实现 cache.SourceInjector)时退回内层的构造期 Source——能力缺失是内层的事实,不是装饰器引入的偏差。

func (*SingleFlightCache[T]) GetOrLoadWithSourceTTL added in v0.24.0

func (sf *SingleFlightCache[T]) GetOrLoadWithSourceTTL(ctx context.Context, key string, source SourceFuncWithTTL[T], ttl time.Duration) (*T, error)

GetOrLoadWithSourceTTL 按调用注入的 source 回源并按 ttl 回填(pkg#77, 带 singleflight 保护)。

与 GetOrLoadWithSource 的差别:回源函数可携带动态 TTL(SourceFuncWithTTL), 且 ttl > 0 时覆盖回源给出的 TTL(ttl <= 0 时按回源给出的 TTL,仍无则 退回构造期默认 TTL)。

内层不支持按调用注入(未实现 cache.SourceInjector)时退回内层的构造期 回源;此时 ttl > 0 仍生效(覆盖构造期回源给出的 TTL)。

func (*SingleFlightCache[T]) MGet added in v0.6.0

func (sf *SingleFlightCache[T]) MGet(ctx context.Context, keys ...string) (map[string]*T, error)

MGet 批量获取:内层具备批量能力时直接委托(装饰器不得退化批量); 内层无批量能力时退化为逐键 Get。

func (*SingleFlightCache[T]) Peek added in v0.23.0

func (sf *SingleFlightCache[T]) Peek(ctx context.Context, key string) (*T, bool, error)

Peek 非删除式读取(pkg#72):读到过期条目时**不删除**它,只是不命中。

装饰器必须透明:内层支持 cache.PeekGetter 时原样委托;否则退化为 Get 语义(内层可能惰性删除过期条目——回收口径由内层决定)。内层不支持时 仍返回本方法,是为了让「装饰器不丢能力」这条约束成立:外层再包一层 装饰器时,前置检查仍能走非删除式读路径,不会再次废掉 stale-if-error 兜底(内层的 Peek 缺失时,兜底本就无从谈起——退化是能力缺失,不是 装饰器引入的偏差)。

命中语义与内层的 Peek 一致:hit=false 表示未命中(不存在/已过期/null marker);hit=true 时返回值可能为 nil(命中但值为 nil)。

func (*SingleFlightCache[T]) Refresh added in v0.4.0

func (sf *SingleFlightCache[T]) Refresh(ctx context.Context, key string) (*T, error)

Refresh 刷新缓存(带 singleflight 保护)。

func (*SingleFlightCache[T]) Set added in v0.4.0

func (sf *SingleFlightCache[T]) Set(ctx context.Context, key string, value *T) error

Set 设置缓存。

func (*SingleFlightCache[T]) SetWithTTL added in v0.4.0

func (sf *SingleFlightCache[T]) SetWithTTL(ctx context.Context, key string, value *T, ttl time.Duration) error

SetWithTTL 设置缓存(自定义 TTL)。

func (*SingleFlightCache[T]) Stats added in v0.24.0

func (sf *SingleFlightCache[T]) Stats() Stats

Stats 返回内层缓存的观测计数快照(pkg#76)。

装饰器不得丢能力:内层实现 cache.StatsProvider 时原样委托;否则返回零值 快照——能力缺失是内层的事实(同 MGet/Peek 的退化口径)。

计数口径(pkg#90):装饰器的前置检查计入内层计数,回源加载路径不再重复 计未命中 ⇒ 一次读恰好计入命中或未命中之一(与内层直连一致)。

type SourceFunc added in v0.4.0

type SourceFunc[T any] func(ctx context.Context, key string) (*T, error)

SourceFunc 缓存未命中时的数据获取函数。

返回 (nil, nil) 表示数据不存在——实现会缓存 null marker 防穿透: 后续 GetOrLoad 命中 null marker 时直接返回 (nil, nil),不再调用 Source; Exists 对 null marker 返回 true。

null 标记的 TTL 语义由各实现的配置定义 (LocalCache:NullTTL 0 = 默认 1 分钟,负值 = 显式禁用空值缓存)。

Source 返回的 *T 所有权随值语义契约:实现回填后调用方与实现共享引用 或各持副本,取决于实现(同"实现契约"的值语义,调用方一律只读对待)。

type SourceFuncWithTTL added in v0.14.0

type SourceFuncWithTTL[T any] func(ctx context.Context, key string) (*T, time.Duration, error)

SourceFuncWithTTL 携带动态回填 TTL 的数据获取函数(pkg#11)。 ttl > 0 按其缓存;ttl <= 0 回退实现配置的默认 TTL; 返回 (nil, ttl, nil) 仍走 null marker 防穿透(此时 ttl 不生效,null marker 的 TTL 由实现配置治理——LocalCache 为 NullTTL)。 典型场景:第三方 token 的 expires_in、上游按内容给出的缓存窗口。

type SourceInjector added in v0.24.0

type SourceInjector[T any] interface {
	// GetOrLoadWithSource 按调用注入的 source 回源(source 为 nil 时退回
	// 构造期 Source/SourceWithTTL)。
	GetOrLoadWithSource(ctx context.Context, key string, source SourceFunc[T]) (*T, error)

	// GetOrLoadWithSourceTTL 按调用注入的 source 回源并按 ttl 回填
	// (source 为 nil 时退回构造期回源;ttl <= 0 时按回源给出的 TTL,
	// 仍无则退回构造期默认 TTL)。
	GetOrLoadWithSourceTTL(ctx context.Context, key string, source SourceFuncWithTTL[T], ttl time.Duration) (*T, error)
}

SourceInjector 是可选能力接口:支持「按调用注入回源函数」的缓存实现 (LocalCache 实现)。

面向「回源依赖 key / 依赖调用上下文」的缓存形态(token / 凭据 / 租户级 缓存):构造期固定的 Source 只能覆盖「一个函数管全部 key」,按调用注入则 让调用方在**调用点**给出本次回源函数——不必在缓存外再包一层 key→source 注册表(那等于又自己写了一份缓存)。

注意:构造期 Source 本就收 key(SourceFunc 形如 func(ctx, key)), 「按 key 分派」一直可做;本接口补的是**按调用注入**这一层—— 同一 key 的不同调用方可以给出不同的回源函数。

未实现本接口的后端:装饰器(SingleFlightCache)退回内层的构造期 Source ——能力缺失是内层的事实,不是装饰器引入的偏差。

type Stats added in v0.24.0

type Stats struct {
	Hits        int64
	Misses      int64
	Evictions   int64
	Expirations int64
	Size        int64
}

Stats 是缓存的观测计数快照(pkg#76)。

口径(LocalCache 实现;各计数单调不减,只增不减):

  • Hits:读命中次数——读到**未过期的真值条目**。计入的读路径: Get / GetEx / Peek / MGet(逐键)/ GetOrLoad(含按调用注入回源的变体)。
  • Misses:读未命中次数——key 不存在、已过期,或命中 null marker。 null marker 表示「数据不存在」、无值可返 ⇒ 计入未命中(与 GetEx/Peek 的 hit=false 口径一致)。
  • Evictions:容量淘汰次数——MaxSize 触发的 FIFO/LRU 淘汰。
  • Expirations:TTL 过期回收次数——过期条目被移除(惰性删除或清扫)。
  • Size:快照时刻的条目数。

**不计入**:Exists(存在性探测,不是取值读路径)、Set/Del(写路径)。

快照语义:各计数字段分别原子读取、Size 在读锁下取——并发读写下快照是 近似一致的(各字段可能来自略有差别的时刻),但计数本身单调不减。

装饰器叠加的口径(pkg#90 修正):SingleFlightCache 的 GetOrLoad 先做一次 非删除式前置检查(Peek)——该次读计入内层计数;未命中时内层的回源加载 路径**不再重复计未命中** ⇒ 一次读恰好计入命中或未命中之一,与内层直连 口径一致(修正前未命中被计两次,命中率指标失真:实测 90% 的真实命中率 被报成 81.8%)。装饰器自身不另设计数,Stats 原样透出内层快照。

该口径依赖内层提供「加载不计未命中」的能力(pkg 内 LocalCache 提供); 内层不提供时装饰器退回内层原有口径(未命中被计两次)——能力缺失是内层 的事实,不是装饰器改写计数语义。

type StatsProvider added in v0.24.0

type StatsProvider interface {
	// Stats 返回当前观测计数快照。
	Stats() Stats
}

StatsProvider 是可选能力接口:暴露缓存的观测计数快照(LocalCache 实现)。

消费方经类型断言探测选用(与 BatchGetter/Sweeper/HitGetter/PeekGetter 同款「可选能力接口 + 类型断言」哲学)——不加入 Cache[T],不破坏其它后端 实现。典型用法:Prometheus GaugeFunc 在采集时读 Stats() 导出命中率。

type Sweeper added in v0.16.0

type Sweeper interface {
	// Sweep 清扫过期条目,返回本次清除的条目数。
	Sweep(ctx context.Context) int
}

Sweeper 是可选能力接口:支持手动清扫过期条目(LocalCache 实现)。

供「不起后台清扫协程」的部署形态使用——短命命令行/宿主内嵌场景 由调用方在合适时机调用 Sweep 回收过期条目,避免为缓存常驻后台 goroutine。实现须保证 Sweep 可重复调用(幂等:已回收的条目不再计数)。

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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