feat(cache): 添加 stale_if_error 和 stale_if_timeout 缓存接口和实现

在 CacheBackend 接口新增 GetStale 方法,支持上游错误时按错误类型
(超时 vs 其他错误)检查对应的 stale 窗口返回过期缓存。
ProxyCache、DiskCache、TieredCache 均实现该方法。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-24 10:05:39 +08:00
parent c6df735caa
commit 8deda73b24
4 changed files with 225 additions and 29 deletions

View File

@ -34,6 +34,21 @@ type CacheBackend interface {
// - bool: 是否过期staletrue 表示可使用过期缓存
Get(hashKey uint64, origKey string) (entry *ProxyCacheEntry, exists bool, stale bool)
// GetStale 在上游错误时获取可用的过期缓存。
//
// 与 Get 不同GetStale 只在错误发生时使用,根据错误类型检查对应的 stale 窗口。
// 超时错误检查 staleIfTimeout 窗口,其他错误检查 staleIfError 窗口。
//
// 参数:
// - hashKey: 缓存键的哈希值
// - origKey: 原始缓存键(用于双重验证)
// - isTimeout: 是否为超时错误
//
// 返回值:
// - *ProxyCacheEntry: 缓存条目
// - bool: 是否存在可用的过期缓存
GetStale(hashKey uint64, origKey string, isTimeout bool) (entry *ProxyCacheEntry, exists bool)
// Set 设置缓存条目。
//
// 无返回值,与现有 ProxyCache.Set 签名一致。

View File

@ -37,18 +37,26 @@ type DiskCacheConfig struct {
// Inactive 未访问淘汰时间
Inactive time.Duration
// StaleIfError 错误时使用过期缓存的窗口
StaleIfError time.Duration
// StaleIfTimeout 超时时使用过期缓存的窗口
StaleIfTimeout time.Duration
}
// DiskCache 磁盘缓存实现。
type DiskCache struct {
basePath string
levels []int
maxSize int64
inactive time.Duration
currentSize atomic.Int64
entries map[uint64]*DiskCacheMeta
mu sync.RWMutex
stopCh chan struct{}
basePath string
levels []int
maxSize int64
inactive time.Duration
staleIfError time.Duration
staleIfTimeout time.Duration
currentSize atomic.Int64
entries map[uint64]*DiskCacheMeta
mu sync.RWMutex
stopCh chan struct{}
// 懒加载相关
loaded atomic.Bool
@ -90,13 +98,15 @@ func NewDiskCache(cfg *DiskCacheConfig) (*DiskCache, error) {
}
dc := &DiskCache{
basePath: cfg.Path,
levels: parseLevels(cfg.Levels),
maxSize: cfg.MaxSize,
inactive: cfg.Inactive,
entries: make(map[uint64]*DiskCacheMeta),
loadCh: make(chan struct{}),
stopCh: make(chan struct{}),
basePath: cfg.Path,
levels: parseLevels(cfg.Levels),
maxSize: cfg.MaxSize,
inactive: cfg.Inactive,
staleIfError: cfg.StaleIfError,
staleIfTimeout: cfg.StaleIfTimeout,
entries: make(map[uint64]*DiskCacheMeta),
loadCh: make(chan struct{}),
stopCh: make(chan struct{}),
}
// 启动后台加载,不阻塞服务启动
@ -241,6 +251,92 @@ func (dc *DiskCache) Get(hashKey uint64, origKey string) (*ProxyCacheEntry, bool
return entry, true, stale
}
// GetStale 在上游错误时获取可用的过期缓存。
//
// 与 Get 不同GetStale 只在错误发生时使用,根据错误类型检查对应的 stale 窗口。
func (dc *DiskCache) GetStale(hashKey uint64, origKey string, isTimeout bool) (*ProxyCacheEntry, bool) {
// 等待懒加载完成
if !dc.loaded.Load() {
select {
case <-dc.loadCh:
case <-time.After(100 * time.Millisecond):
return nil, false
}
}
dc.mu.RLock()
meta, ok := dc.entries[hashKey]
dc.mu.RUnlock()
if !ok {
return nil, false
}
// 双重验证:检查原始 key 是否匹配
if meta.OrigKey != origKey {
return nil, false
}
// 读取数据文件
dataPath := dc.filePathFromHash(hashKey, "data")
data, err := os.ReadFile(dataPath)
if err != nil {
return nil, false
}
// 验证 CRC32
crc := crc32.ChecksumIEEE(data)
if crc != meta.CRC32 {
return nil, false
}
now := time.Now()
expiresAt := meta.Created.Add(meta.MaxAge)
// 未过期,直接返回
if !now.After(expiresAt) {
entry := &ProxyCacheEntry{
Key: meta.OrigKey,
OrigKey: meta.OrigKey,
Data: data,
Headers: meta.Headers,
Status: meta.Status,
Created: meta.Created,
MaxAge: meta.MaxAge,
}
return entry, true
}
// 已过期,检查 stale 窗口
var staleWindow time.Duration
if isTimeout {
staleWindow = dc.staleIfTimeout
} else {
staleWindow = dc.staleIfError
}
if staleWindow <= 0 {
return nil, false
}
// 检查是否在 stale 窗口内
if now.Sub(expiresAt) > staleWindow {
return nil, false
}
entry := &ProxyCacheEntry{
Key: meta.OrigKey,
OrigKey: meta.OrigKey,
Data: data,
Headers: meta.Headers,
Status: meta.Status,
Created: meta.Created,
MaxAge: meta.MaxAge,
}
return entry, true
}
// Set 设置缓存条目(实现 CacheBackend 接口)。
func (dc *DiskCache) Set(hashKey uint64, origKey string, data []byte, headers map[string]string, status int, maxAge time.Duration) {
// 计算文件路径

View File

@ -310,12 +310,14 @@ type ProxyCacheEntry struct {
// ProxyCache 代理响应缓存,支持缓存锁防击穿。
type ProxyCache struct {
entries map[uint64]*ProxyCacheEntry
pending map[uint64]*pendingRequest
rules []ProxyCacheRule
staleTime time.Duration
mu sync.RWMutex
cacheLock bool
entries map[uint64]*ProxyCacheEntry
pending map[uint64]*pendingRequest
rules []ProxyCacheRule
staleTime time.Duration // StaleWhileRevalidate 窗口
staleIfError time.Duration // 错误时使用过期缓存的窗口
staleIfTimeout time.Duration // 超时时使用过期缓存的窗口
mu sync.RWMutex
cacheLock bool
}
// pendingRequest 等待中的缓存请求。
@ -332,17 +334,21 @@ type pendingRequest struct {
// 参数:
// - rules: 代理缓存规则列表,定义可缓存的路径、方法、状态码等
// - cacheLock: 是否启用缓存生成锁,防止多个请求同时生成缓存
// - staleTime: 过期缓存可复用的额外时间,设为 0 表示不启用
// - staleTime: StaleWhileRevalidate 窗口,过期后后台刷新期间可复用
// - staleIfError: 错误时使用过期缓存的窗口(上游 5xx/连接失败)
// - staleIfTimeout: 超时时使用过期缓存的窗口
//
// 返回值:
// - *ProxyCache: 初始化的代理缓存实例
func NewProxyCache(rules []ProxyCacheRule, cacheLock bool, staleTime time.Duration) *ProxyCache {
func NewProxyCache(rules []ProxyCacheRule, cacheLock bool, staleTime, staleIfError, staleIfTimeout time.Duration) *ProxyCache {
return &ProxyCache{
rules: rules,
entries: make(map[uint64]*ProxyCacheEntry),
cacheLock: cacheLock,
pending: make(map[uint64]*pendingRequest),
staleTime: staleTime,
rules: rules,
entries: make(map[uint64]*ProxyCacheEntry),
cacheLock: cacheLock,
pending: make(map[uint64]*pendingRequest),
staleTime: staleTime,
staleIfError: staleIfError,
staleIfTimeout: staleIfTimeout,
}
}
@ -380,6 +386,61 @@ func (c *ProxyCache) Get(hashKey uint64, origKey string) (*ProxyCacheEntry, bool
return entry, true, false
}
// GetStale 在上游错误时获取可用的过期缓存。
//
// 与 Get 不同GetStale 只在错误发生时使用,根据错误类型检查对应的 stale 窗口。
// 超时错误检查 staleIfTimeout其他错误检查 staleIfError。
//
// 参数:
// - hashKey: 缓存键的哈希值
// - origKey: 原始缓存键(用于双重验证)
// - isTimeout: 是否为超时错误
//
// 返回值:
// - *ProxyCacheEntry: 缓存条目
// - bool: 是否存在可用的过期缓存
func (c *ProxyCache) GetStale(hashKey uint64, origKey string, isTimeout bool) (*ProxyCacheEntry, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[hashKey]
if !ok {
return nil, false
}
// 双重验证:检查原始 key 是否匹配
if entry.OrigKey != origKey {
return nil, false
}
now := time.Now()
expiresAt := entry.Created.Add(entry.MaxAge)
// 未过期,直接返回
if !now.After(expiresAt) {
return entry, true
}
// 已过期,检查 stale 窗口
var staleWindow time.Duration
if isTimeout {
staleWindow = c.staleIfTimeout
} else {
staleWindow = c.staleIfError
}
if staleWindow <= 0 {
return nil, false
}
// 检查是否在 stale 窗口内
if now.Sub(expiresAt) > staleWindow {
return nil, false
}
return entry, true
}
// Set 设置代理缓存条目。
func (c *ProxyCache) Set(hashKey uint64, origKey string, data []byte, headers map[string]string, status int, maxAge time.Duration) {
c.mu.Lock()

View File

@ -28,6 +28,10 @@ type TieredCacheConfig struct {
// L2 配置
L2Config *DiskCacheConfig
// Stale 配置
StaleIfError time.Duration // 错误时使用过期缓存的窗口
StaleIfTimeout time.Duration // 超时时使用过期缓存的窗口
// 热点提升配置
PromoteThreshold int // 访问次数阈值,超过后提升到 L1
PromoteInterval time.Duration // 提升检查间隔
@ -66,7 +70,7 @@ type accessInfo struct {
// NewTieredCache 创建分层缓存实例。
func NewTieredCache(cfg *TieredCacheConfig) (*TieredCache, error) {
// 创建 L1 内存缓存
l1 := NewProxyCache(nil, true, 0)
l1 := NewProxyCache(nil, true, 0, cfg.StaleIfError, cfg.StaleIfTimeout)
// 创建 L2 磁盘缓存
l2, err := NewDiskCache(cfg.L2Config)
@ -123,6 +127,26 @@ func (tc *TieredCache) Get(hashKey uint64, origKey string) (*ProxyCacheEntry, bo
return entry, true, stale
}
// GetStale 在上游错误时获取可用的过期缓存。
//
// 先查 L1再查 L2。
func (tc *TieredCache) GetStale(hashKey uint64, origKey string, isTimeout bool) (*ProxyCacheEntry, bool) {
// 1. 先查 L1
if entry, ok := tc.l1.GetStale(hashKey, origKey, isTimeout); ok {
tc.l1Hits.Add(1)
return entry, true
}
// 2. 查 L2
if entry, ok := tc.l2.GetStale(hashKey, origKey, isTimeout); ok {
tc.l2Hits.Add(1)
return entry, true
}
tc.misses.Add(1)
return nil, false
}
// Set 设置缓存条目(实现 CacheBackend 接口)。
func (tc *TieredCache) Set(hashKey uint64, origKey string, data []byte, headers map[string]string, status int, maxAge time.Duration) {
// 同时写入 L1 和 L2