From 8deda73b24df5a15643bffbffc27074fabb9cbc6 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 24 Apr 2026 10:05:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(cache):=20=E6=B7=BB=E5=8A=A0=20stale=5Fif?= =?UTF-8?q?=5Ferror=20=E5=92=8C=20stale=5Fif=5Ftimeout=20=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 CacheBackend 接口新增 GetStale 方法,支持上游错误时按错误类型 (超时 vs 其他错误)检查对应的 stale 窗口返回过期缓存。 ProxyCache、DiskCache、TieredCache 均实现该方法。 Co-Authored-By: Claude Opus 4.7 --- internal/cache/backend.go | 15 ++++ internal/cache/disk_cache.go | 126 +++++++++++++++++++++++++++++---- internal/cache/file_cache.go | 87 +++++++++++++++++++---- internal/cache/tiered_cache.go | 26 ++++++- 4 files changed, 225 insertions(+), 29 deletions(-) diff --git a/internal/cache/backend.go b/internal/cache/backend.go index 13f299a..3b5e44b 100644 --- a/internal/cache/backend.go +++ b/internal/cache/backend.go @@ -34,6 +34,21 @@ type CacheBackend interface { // - bool: 是否过期(stale),true 表示可使用过期缓存 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 签名一致。 diff --git a/internal/cache/disk_cache.go b/internal/cache/disk_cache.go index 0f6c2ab..c895a6f 100644 --- a/internal/cache/disk_cache.go +++ b/internal/cache/disk_cache.go @@ -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) { // 计算文件路径 diff --git a/internal/cache/file_cache.go b/internal/cache/file_cache.go index f7f716c..104efb8 100644 --- a/internal/cache/file_cache.go +++ b/internal/cache/file_cache.go @@ -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() diff --git a/internal/cache/tiered_cache.go b/internal/cache/tiered_cache.go index ee2cfa2..7228233 100644 --- a/internal/cache/tiered_cache.go +++ b/internal/cache/tiered_cache.go @@ -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