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 ¶
- func HashKey(prefix string, args ...any) string
- func Key(parts ...string) string
- func TenantKey(tenantID, suffix string) string
- func UserKey(userID int64, suffix string) string
- type BatchGetter
- type Cache
- type CacheAside
- type Checker
- type EvictPolicy
- type HitGetter
- type LocalCache
- func (c *LocalCache[T]) Clear()
- func (c *LocalCache[T]) Close(ctx context.Context) error
- func (c *LocalCache[T]) Del(ctx context.Context, key string) error
- func (c *LocalCache[T]) DelBatch(ctx context.Context, keys ...string) error
- func (c *LocalCache[T]) Exists(ctx context.Context, key string) (bool, error)
- func (c *LocalCache[T]) Get(ctx context.Context, key string) (*T, error)
- func (c *LocalCache[T]) GetEx(ctx context.Context, key string) (*T, bool, error)
- func (c *LocalCache[T]) GetOrLoad(ctx context.Context, key string) (*T, error)
- func (c *LocalCache[T]) MGet(ctx context.Context, keys ...string) (map[string]*T, error)
- func (c *LocalCache[T]) Refresh(ctx context.Context, key string) (*T, error)
- func (c *LocalCache[T]) Set(ctx context.Context, key string, value *T) error
- func (c *LocalCache[T]) SetWithTTL(ctx context.Context, key string, value *T, ttl time.Duration) error
- func (c *LocalCache[T]) Size() int
- func (c *LocalCache[T]) Sweep(ctx context.Context) int
- type LocalConfig
- type Option
- type SingleFlightCache
- func (sf *SingleFlightCache[T]) Del(ctx context.Context, key string) error
- func (sf *SingleFlightCache[T]) DelBatch(ctx context.Context, keys ...string) error
- func (sf *SingleFlightCache[T]) Exists(ctx context.Context, key string) (bool, error)
- func (sf *SingleFlightCache[T]) Get(ctx context.Context, key string) (*T, error)
- func (sf *SingleFlightCache[T]) GetOrLoad(ctx context.Context, key string) (*T, error)
- func (sf *SingleFlightCache[T]) MGet(ctx context.Context, keys ...string) (map[string]*T, error)
- func (sf *SingleFlightCache[T]) Refresh(ctx context.Context, key string) (*T, error)
- func (sf *SingleFlightCache[T]) Set(ctx context.Context, key string, value *T) error
- func (sf *SingleFlightCache[T]) SetWithTTL(ctx context.Context, key string, value *T, ttl time.Duration) error
- type SourceFunc
- type SourceFuncWithTTL
- type Sweeper
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HashKey ¶ added in v0.4.0
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
Key 用冒号连接各部分生成缓存 key。
cache.Key("user", "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 感知,不叠加刷新轮次)。并发安全。
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]) 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]) 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
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 取到的值)不算读命中——那是调用方 自己的值,不克隆。
func (*LocalCache[T]) MGet ¶ added in v0.6.0
MGet 批量获取:单次读锁区间取回一批键(契约见 BatchGetter——缺席语义 与 (nil, nil) 双关一致:不存在与 null marker 一律缺席;纯读不回填, 已过期条目按缺席处理,回收留给 Get/Exists 的双检路径——TTL=0 时无 cleanup goroutine,过期条目仅在这些路径被惰性回收)。
CloneOnRead 开启时按配置返回副本(pkg#25);EvictLRU 策略下批量命中同样 提升顺序(读锁区间之外一次性提升,避免逐键取写锁)。
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]) 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 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]) 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。
流程:
- 查缓存,命中则直接返回
- 未命中时通过 singleflight 去重,只有一个 goroutine 调用 Source
- Source 返回后回填缓存
- 其他 goroutine 共享同一份结果
func (*SingleFlightCache[T]) MGet ¶ added in v0.6.0
MGet 批量获取:内层具备批量能力时直接委托(装饰器不得退化批量); 内层无批量能力时退化为逐键 Get。
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)。
type SourceFunc ¶ added in v0.4.0
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
SourceFuncWithTTL 携带动态回填 TTL 的数据获取函数(pkg#11)。 ttl > 0 按其缓存;ttl <= 0 回退实现配置的默认 TTL; 返回 (nil, ttl, nil) 仍走 null marker 防穿透(此时 ttl 不生效,null marker 的 TTL 由实现配置治理——LocalCache 为 NullTTL)。 典型场景:第三方 token 的 expires_in、上游按内容给出的缓存窗口。