789 lines
21 KiB
Go
789 lines
21 KiB
Go
// Package cache 提供文件缓存和代理缓存功能,支持 LRU 淘汰和缓存锁防击穿。
|
||
//
|
||
// 该文件包含缓存相关的核心逻辑,包括:
|
||
// - 文件缓存实现,支持 LRU 淘汰策略
|
||
// - 代理响应缓存,支持缓存锁防止缓存击穿
|
||
// - 缓存统计和生命周期管理
|
||
//
|
||
// 主要用途:
|
||
//
|
||
// 用于缓存静态文件内容和代理响应,减少磁盘 I/O 和上游请求,提升服务性能。
|
||
//
|
||
// 注意事项:
|
||
// - 文件缓存支持按条目数和内存大小双重限制
|
||
// - 代理缓存支持过期缓存复用(stale)机制
|
||
// - 所有公开方法均为并发安全
|
||
//
|
||
// 作者:xfy
|
||
package cache
|
||
|
||
import (
|
||
"container/list"
|
||
"hash/fnv"
|
||
"slices"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/utils"
|
||
)
|
||
|
||
// FileEntry 文件缓存条目,存储单个文件的缓存信息。
|
||
type FileEntry struct {
|
||
ModTime time.Time
|
||
CachedAt time.Time // 缓存时间,用于 TTL 验证(新鲜度)
|
||
LastAccess time.Time
|
||
element *list.Element
|
||
Path string
|
||
Data []byte
|
||
Size int64
|
||
ETag string // 预计算的 ETag,避免每次请求重新计算
|
||
ContentType string // 预计算的 MIME 类型,避免每次请求重新检测
|
||
}
|
||
|
||
// FileCache 文件缓存,支持 LRU 淘汰策略。
|
||
//
|
||
// 该结构体实现了基于内存的文件缓存,支持按条目数和内存大小限制进行淘汰。
|
||
// 使用 LRU(最近最少使用)算法决定淘汰顺序。
|
||
//
|
||
// 注意事项:
|
||
// - 所有方法均为并发安全
|
||
// - 支持过期时间自动淘汰
|
||
// - 使用 sync.Pool 复用 FileEntry,减少内存分配
|
||
type FileCache struct {
|
||
entries map[string]*FileEntry
|
||
lruList *list.List
|
||
maxEntries int64
|
||
maxSize int64
|
||
inactive time.Duration
|
||
currentSize int64
|
||
mu sync.RWMutex
|
||
entryPool sync.Pool // FileEntry 池,复用条目减少分配
|
||
}
|
||
|
||
// NewFileCache 创建文件缓存实例。
|
||
//
|
||
// 根据指定的条目数限制、内存大小限制和过期时间创建缓存。
|
||
//
|
||
// 参数:
|
||
// - maxEntries: 最大缓存条目数,设为 0 表示不限制
|
||
// - maxSize: 内存使用上限(字节),设为 0 表示不限制
|
||
// - inactive: 未访问淘汰时间,超过此时间未访问的条目将被淘汰
|
||
//
|
||
// 返回值:
|
||
// - *FileCache: 创建的文件缓存实例
|
||
func NewFileCache(maxEntries, maxSize int64, inactive time.Duration) *FileCache {
|
||
c := &FileCache{
|
||
maxEntries: maxEntries,
|
||
maxSize: maxSize,
|
||
inactive: inactive,
|
||
entries: make(map[string]*FileEntry),
|
||
lruList: list.New(),
|
||
}
|
||
// 初始化 entry 池
|
||
c.entryPool = sync.Pool{
|
||
New: func() any {
|
||
return &FileEntry{}
|
||
},
|
||
}
|
||
return c
|
||
}
|
||
|
||
// Get 获取缓存的文件。
|
||
//
|
||
// 根据文件路径查找缓存条目,如果找到且未过期则返回。
|
||
// 访问时会更新条目的访问时间并移动到 LRU 链表头部。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
//
|
||
// 返回值:
|
||
// - *FileEntry: 缓存条目,包含文件内容和元数据
|
||
// - bool: 是否找到有效缓存
|
||
func (c *FileCache) Get(path string) (*FileEntry, bool) {
|
||
c.mu.RLock()
|
||
|
||
entry, ok := c.entries[path]
|
||
if !ok {
|
||
c.mu.RUnlock()
|
||
return nil, false
|
||
}
|
||
|
||
// 只读判断:检查是否过期
|
||
if time.Since(entry.LastAccess) > c.inactive {
|
||
c.mu.RUnlock()
|
||
// 升级为写锁,double-check entry 仍存在且仍过期
|
||
c.mu.Lock()
|
||
if entry, ok = c.entries[path]; ok && time.Since(entry.LastAccess) > c.inactive {
|
||
c.removeEntry(entry)
|
||
}
|
||
c.mu.Unlock()
|
||
return nil, false
|
||
}
|
||
|
||
// 只读判断:CachedAt 是否需要迁移
|
||
needMigrate := entry.CachedAt.IsZero()
|
||
|
||
c.mu.RUnlock()
|
||
|
||
if needMigrate {
|
||
// 升级为写锁,double-check entry 仍存在
|
||
c.mu.Lock()
|
||
entry, ok = c.entries[path]
|
||
if !ok {
|
||
c.mu.Unlock()
|
||
return nil, false
|
||
}
|
||
// double-check 过期(RUnlock 和 Lock 之间可能已过期)
|
||
if time.Since(entry.LastAccess) > c.inactive {
|
||
c.removeEntry(entry)
|
||
c.mu.Unlock()
|
||
return nil, false
|
||
}
|
||
entry.CachedAt = time.Now()
|
||
entry.LastAccess = time.Now()
|
||
c.lruList.MoveToFront(entry.element)
|
||
c.mu.Unlock()
|
||
return entry, true
|
||
}
|
||
|
||
// 近似 LRU: 不更新 LastAccess/LRU 位置,直接返回
|
||
// 精确 LRU 由 Set/Delete/evict 操作保证
|
||
return entry, true
|
||
}
|
||
|
||
// Set 设置缓存条目。
|
||
//
|
||
// 将文件内容存入缓存,如果缓存已存在则更新。
|
||
// 存入后检查是否需要触发 LRU 淘汰。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
// - data: 文件内容字节
|
||
// - size: 文件大小(字节)
|
||
// - modTime: 文件最后修改时间
|
||
// - contentType: MIME 类型
|
||
//
|
||
// 返回值:
|
||
// - error: 当前实现始终返回 nil
|
||
func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time, contentType string) error {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
// 预计算 ETag
|
||
etag := utils.GenerateETag(modTime, size)
|
||
|
||
// 检查是否已存在
|
||
if entry, ok := c.entries[path]; ok {
|
||
c.currentSize -= entry.Size
|
||
entry.Data = data
|
||
entry.Size = size
|
||
entry.ModTime = modTime
|
||
entry.ETag = etag
|
||
entry.ContentType = contentType
|
||
entry.CachedAt = time.Now() // 更新缓存时间
|
||
entry.LastAccess = time.Now()
|
||
c.currentSize += size
|
||
c.lruList.MoveToFront(entry.element)
|
||
c.evictIfNeeded()
|
||
return nil
|
||
}
|
||
|
||
// 从池中获取条目并初始化
|
||
entry := c.entryPool.Get().(*FileEntry) //nolint:errcheck // pool always returns valid *FileEntry
|
||
entry.Path = path
|
||
entry.Data = data
|
||
entry.Size = size
|
||
entry.ModTime = modTime
|
||
entry.ETag = etag
|
||
entry.ContentType = contentType
|
||
entry.CachedAt = time.Now()
|
||
entry.LastAccess = time.Now()
|
||
entry.element = c.lruList.PushFront(entry)
|
||
c.entries[path] = entry
|
||
c.currentSize += size
|
||
|
||
c.evictIfNeeded()
|
||
return nil
|
||
}
|
||
|
||
// Delete 删除缓存条目。
|
||
//
|
||
// 根据文件路径删除对应的缓存条目。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
func (c *FileCache) Delete(path string) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if entry, ok := c.entries[path]; ok {
|
||
c.removeEntry(entry)
|
||
}
|
||
}
|
||
|
||
// RefreshCachedAt 更新 CachedAt 并移动 LRU 位置。
|
||
//
|
||
// 用于 TTL 过期但文件未修改时刷新缓存时间。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
func (c *FileCache) RefreshCachedAt(path string) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if entry, ok := c.entries[path]; ok {
|
||
entry.CachedAt = time.Now()
|
||
entry.LastAccess = time.Now()
|
||
c.lruList.MoveToFront(entry.element)
|
||
}
|
||
}
|
||
|
||
// removeEntry 内部删除条目(不加锁)。
|
||
//
|
||
// 从 LRU 链表和条目映射中移除指定条目,更新当前内存使用量。
|
||
// 调用此方法前必须已持有写锁。
|
||
//
|
||
// 参数:
|
||
// - entry: 要删除的缓存条目
|
||
func (c *FileCache) removeEntry(entry *FileEntry) {
|
||
c.lruList.Remove(entry.element)
|
||
delete(c.entries, entry.Path)
|
||
c.currentSize -= entry.Size
|
||
// Reset entry 并放回池中复用
|
||
entry.Path = ""
|
||
entry.Data = nil
|
||
entry.Size = 0
|
||
entry.ModTime = time.Time{}
|
||
entry.CachedAt = time.Time{}
|
||
entry.LastAccess = time.Time{}
|
||
entry.ETag = ""
|
||
entry.ContentType = ""
|
||
entry.element = nil
|
||
c.entryPool.Put(entry)
|
||
}
|
||
|
||
// evictIfNeeded 根据限制淘汰条目。
|
||
//
|
||
// 检查当前缓存是否超过条目数或内存大小限制,
|
||
// 如果超过则调用 evictLRU 淘汰最久未使用的条目。
|
||
func (c *FileCache) evictIfNeeded() {
|
||
// 按条目数淘汰
|
||
for c.lruList.Len() > int(c.maxEntries) && c.maxEntries > 0 {
|
||
c.evictLRU()
|
||
}
|
||
|
||
// 按内存大小淘汰
|
||
for c.currentSize > c.maxSize && c.maxSize > 0 {
|
||
c.evictLRU()
|
||
}
|
||
}
|
||
|
||
// evictLRU 淘汰最久未使用的条目。
|
||
//
|
||
// 从 LRU 链表尾部移除条目并删除。
|
||
// 如果链表为空则不执行任何操作。
|
||
func (c *FileCache) evictLRU() {
|
||
if c.lruList.Len() == 0 {
|
||
return
|
||
}
|
||
|
||
element := c.lruList.Back()
|
||
if element == nil {
|
||
return
|
||
}
|
||
|
||
entry, ok := element.Value.(*FileEntry)
|
||
if !ok {
|
||
return
|
||
}
|
||
c.removeEntry(entry)
|
||
}
|
||
|
||
// Stats 返回缓存统计信息。
|
||
func (c *FileCache) Stats() FileCacheStats {
|
||
c.mu.RLock()
|
||
defer c.mu.RUnlock()
|
||
|
||
return FileCacheStats{
|
||
Entries: int64(len(c.entries)),
|
||
MaxEntries: c.maxEntries,
|
||
Size: c.currentSize,
|
||
MaxSize: c.maxSize,
|
||
}
|
||
}
|
||
|
||
// FileCacheStats 文件缓存统计。
|
||
type FileCacheStats struct {
|
||
// Entries 当前缓存条目数量
|
||
Entries int64
|
||
|
||
// MaxEntries 最大缓存条目数限制
|
||
MaxEntries int64
|
||
|
||
// Size 当前缓存使用的内存大小(字节)
|
||
Size int64
|
||
|
||
// MaxSize 最大内存使用限制(字节)
|
||
MaxSize int64
|
||
}
|
||
|
||
// ProxyCacheRule 代理缓存规则。
|
||
type ProxyCacheRule struct {
|
||
Path string // 匹配路径
|
||
Methods []string // 可缓存的 HTTP 方法
|
||
Statuses []int // 可缓存的状态码
|
||
MaxAge time.Duration // 缓存有效期
|
||
}
|
||
|
||
// ProxyCacheEntry 代理缓存条目。
|
||
type ProxyCacheEntry struct {
|
||
Created time.Time
|
||
Headers map[string]string
|
||
Key string
|
||
OrigKey string
|
||
Data []byte
|
||
Status int
|
||
MaxAge time.Duration
|
||
Uses atomic.Int32 // 访问计数,用于 min_uses 阈值检查
|
||
Updating atomic.Bool // 后台更新标志,表示正在后台刷新
|
||
LastModified string // Last-Modified 响应头,用于条件请求
|
||
ETag string // ETag 响应头,用于条件请求
|
||
LastValidated time.Time // 最后验证时间,用于防止验证循环
|
||
}
|
||
|
||
// ProxyCache 代理响应缓存,支持缓存锁防击穿。
|
||
type ProxyCache struct {
|
||
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 等待中的缓存请求。
|
||
type pendingRequest struct {
|
||
done chan struct{} // 完成信号
|
||
err error // 生成结果
|
||
}
|
||
|
||
// NewProxyCache 创建代理缓存实例。
|
||
//
|
||
// 根据指定的缓存规则、锁开关和过期复用时间创建 ProxyCache。
|
||
// 启用 cacheLock 可防止缓存击穿,多个并发请求共享同一个生成过程。
|
||
//
|
||
// 参数:
|
||
// - rules: 代理缓存规则列表,定义可缓存的路径、方法、状态码等
|
||
// - cacheLock: 是否启用缓存生成锁,防止多个请求同时生成缓存
|
||
// - staleTime: StaleWhileRevalidate 窗口,过期后后台刷新期间可复用
|
||
// - staleIfError: 错误时使用过期缓存的窗口(上游 5xx/连接失败)
|
||
// - staleIfTimeout: 超时时使用过期缓存的窗口
|
||
//
|
||
// 返回值:
|
||
// - *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,
|
||
staleIfError: staleIfError,
|
||
staleIfTimeout: staleIfTimeout,
|
||
}
|
||
}
|
||
|
||
// Get 获取缓存的代理响应。
|
||
// hashKey 是 uint64 哈希值,origKey 是原始 key 用于碰撞验证。
|
||
func (c *ProxyCache) Get(hashKey uint64, origKey string) (*ProxyCacheEntry, bool, bool) {
|
||
c.mu.RLock()
|
||
entry, ok := c.entries[hashKey]
|
||
c.mu.RUnlock()
|
||
|
||
if !ok {
|
||
return nil, false, false
|
||
}
|
||
|
||
// 双重验证:检查原始 key 是否匹配(防止哈希碰撞)
|
||
if entry.OrigKey != origKey {
|
||
return nil, false, false
|
||
}
|
||
|
||
// 增加访问计数(原子操作,用于 min_uses 阈值检查)
|
||
entry.Uses.Add(1)
|
||
|
||
// 检查是否过期
|
||
now := time.Now()
|
||
expired := now.Sub(entry.Created) > entry.MaxAge
|
||
|
||
if expired {
|
||
// 检查是否可以使用过期缓存
|
||
if c.staleTime > 0 && now.Sub(entry.Created) <= entry.MaxAge+c.staleTime {
|
||
return entry, true, true // stale but usable
|
||
}
|
||
return nil, false, false
|
||
}
|
||
|
||
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()
|
||
defer c.mu.Unlock()
|
||
|
||
c.entries[hashKey] = &ProxyCacheEntry{
|
||
Key: origKey, // 存储原始 key 作为 Key 字段(保持兼容性)
|
||
OrigKey: origKey,
|
||
Data: data,
|
||
Headers: headers,
|
||
Status: status,
|
||
Created: time.Now(),
|
||
MaxAge: maxAge,
|
||
}
|
||
|
||
// 如果有等待的请求,通知它们
|
||
if pending, ok := c.pending[hashKey]; ok {
|
||
pending.err = nil
|
||
close(pending.done)
|
||
delete(c.pending, hashKey)
|
||
}
|
||
}
|
||
|
||
// AcquireLockWithTimeout 获取缓存生成锁(带超时)。
|
||
// 返回值:
|
||
// - waitCh != nil && timedOut == false: 需要等待其他请求完成
|
||
// - waitCh == nil && timedOut == false: 获得锁,应该生成缓存
|
||
// - timedOut == true: 超时,应该放弃缓存直接请求上游
|
||
func (c *ProxyCache) AcquireLockWithTimeout(hashKey uint64, timeout time.Duration) (waitCh <-chan struct{}, timedOut bool) {
|
||
if !c.cacheLock {
|
||
return nil, false // 不使用缓存锁
|
||
}
|
||
|
||
c.mu.Lock()
|
||
// 检查是否已有缓存
|
||
if _, ok := c.entries[hashKey]; ok {
|
||
c.mu.Unlock()
|
||
return nil, false
|
||
}
|
||
|
||
// 检查是否有 pending 请求
|
||
if pending, ok := c.pending[hashKey]; ok {
|
||
c.mu.Unlock()
|
||
// 有其他请求正在生成,需要等待
|
||
if timeout > 0 {
|
||
// 带超时等待
|
||
select {
|
||
case <-pending.done:
|
||
// 刚刚完成,重新检查缓存
|
||
return nil, false
|
||
case <-time.After(timeout):
|
||
// 超时
|
||
return nil, true
|
||
}
|
||
}
|
||
return pending.done, false // 无限等待
|
||
}
|
||
|
||
// 创建新的 pending 请求
|
||
pending := &pendingRequest{
|
||
done: make(chan struct{}),
|
||
}
|
||
c.pending[hashKey] = pending
|
||
c.mu.Unlock()
|
||
return nil, false // 获得锁,应该生成缓存
|
||
}
|
||
|
||
// ReleaseLock 释放缓存生成锁。
|
||
func (c *ProxyCache) ReleaseLock(hashKey uint64, err error) {
|
||
if !c.cacheLock {
|
||
return
|
||
}
|
||
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if pending, ok := c.pending[hashKey]; ok {
|
||
pending.err = err
|
||
close(pending.done)
|
||
delete(c.pending, hashKey)
|
||
}
|
||
}
|
||
|
||
// MatchRule 检查请求是否匹配缓存规则。
|
||
// 路径匹配规则:
|
||
// - 以 "/" 结尾:前缀匹配(如 "/api/" 匹配 "/api/users")
|
||
// - 包含 "*":通配符匹配(如 "/static/*" 匹配 "/static/css/style.css")
|
||
// - 其他:精确匹配或带分隔符的前缀匹配(如 "/api" 匹配 "/api"、"/api/users"、"/api?query")
|
||
func (c *ProxyCache) MatchRule(path, method string, status int) *ProxyCacheRule {
|
||
for _, rule := range c.rules {
|
||
if !c.matchPath(rule.Path, path) {
|
||
continue
|
||
}
|
||
if !c.matchMethod(rule.Methods, method) {
|
||
continue
|
||
}
|
||
if !c.matchStatus(rule.Statuses, status) {
|
||
continue
|
||
}
|
||
return &rule
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// matchPath 检查路径是否匹配规则路径。
|
||
func (c *ProxyCache) matchPath(rulePath, path string) bool {
|
||
if rulePath == "" {
|
||
return true
|
||
}
|
||
|
||
switch {
|
||
case strings.HasSuffix(rulePath, "/"):
|
||
// 前缀匹配:"/api/" 匹配 "/api/users"
|
||
return strings.HasPrefix(path, rulePath)
|
||
case strings.Contains(rulePath, "*"):
|
||
// 通配符匹配:"/static/*" 匹配 "/static/css/style.css"
|
||
return MatchPattern(rulePath, path)
|
||
default:
|
||
// 精确匹配或带分隔符的前缀匹配
|
||
// "/api" 匹配 "/api"、"/api/users"、"/api?query"
|
||
// 但不匹配 "/apiother"
|
||
if path == rulePath {
|
||
return true
|
||
}
|
||
return strings.HasPrefix(path, rulePath+"/") || strings.HasPrefix(path, rulePath+"?")
|
||
}
|
||
}
|
||
|
||
// matchMethod 检查方法是否在允许列表中。
|
||
func (c *ProxyCache) matchMethod(methods []string, method string) bool {
|
||
if len(methods) == 0 {
|
||
return true
|
||
}
|
||
return slices.Contains(methods, method)
|
||
}
|
||
|
||
// matchStatus 检查状态码是否在允许列表中。
|
||
func (c *ProxyCache) matchStatus(statuses []int, status int) bool {
|
||
if len(statuses) == 0 {
|
||
return true
|
||
}
|
||
return slices.Contains(statuses, status)
|
||
}
|
||
|
||
// Delete 删除缓存条目。
|
||
func (c *ProxyCache) Delete(hashKey uint64) error {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
delete(c.entries, hashKey)
|
||
return nil
|
||
}
|
||
|
||
// DeleteByPatternWithMethod 按通配符模式删除缓存条目。
|
||
// method 过滤:检查 entry.OrigKey 是否以 "method:" 前缀开头。
|
||
// 空 method 匹配所有条目。
|
||
func (c *ProxyCache) DeleteByPatternWithMethod(pattern string, method string) int {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
deleted := 0
|
||
for hashKey, entry := range c.entries {
|
||
if MatchPattern(pattern, entry.OrigKey) {
|
||
if method == "" || strings.HasPrefix(entry.OrigKey, method+":") {
|
||
delete(c.entries, hashKey)
|
||
deleted++
|
||
}
|
||
}
|
||
}
|
||
return deleted
|
||
}
|
||
|
||
// RefreshTTL 刷新缓存条目的 TTL(用于 304 响应处理)。
|
||
// 不替换缓存内容,只更新验证时间和验证头。
|
||
// 返回是否成功(条目可能已被驱逐)。
|
||
func (c *ProxyCache) RefreshTTL(hashKey uint64, origKey string, newHeaders map[string]string) bool {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
entry, ok := c.entries[hashKey]
|
||
if !ok || entry.OrigKey != origKey {
|
||
return false // 条目已被驱逐
|
||
}
|
||
|
||
// 更新验证时间(不更新 Created,保持 LRU 顺序)
|
||
entry.LastValidated = time.Now()
|
||
|
||
// 更新验证头(如果提供)
|
||
if newHeaders != nil {
|
||
if lm, ok := newHeaders["Last-Modified"]; ok {
|
||
entry.LastModified = lm
|
||
}
|
||
if et, ok := newHeaders["ETag"]; ok {
|
||
entry.ETag = et
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// SetValidationHeaders 设置缓存条目的验证头(Last-Modified 和 ETag)。
|
||
func (c *ProxyCache) SetValidationHeaders(hashKey uint64, origKey string, lastModified, etag string) bool {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
entry, ok := c.entries[hashKey]
|
||
if !ok || entry.OrigKey != origKey {
|
||
return false
|
||
}
|
||
|
||
entry.LastModified = lastModified
|
||
entry.ETag = etag
|
||
return true
|
||
}
|
||
|
||
// Stats 返回代理缓存统计。
|
||
func (c *ProxyCache) Stats() ProxyCacheStats {
|
||
c.mu.RLock()
|
||
defer c.mu.RUnlock()
|
||
|
||
return ProxyCacheStats{
|
||
Entries: len(c.entries),
|
||
Pending: len(c.pending),
|
||
}
|
||
}
|
||
|
||
// ProxyCacheStats 代理缓存统计。
|
||
type ProxyCacheStats struct {
|
||
// Entries 当前缓存条目数量
|
||
Entries int
|
||
|
||
// Pending 正在等待缓存生成的请求数量
|
||
Pending int
|
||
}
|
||
|
||
// HashPathWithMethod 使用 FNV-64a 计算路径和方法的哈希值。
|
||
func HashPathWithMethod(path string, method string) uint64 {
|
||
if method == "" {
|
||
method = "GET"
|
||
}
|
||
key := method + ":" + path
|
||
h := fnv.New64a()
|
||
h.Write([]byte(key))
|
||
return h.Sum64()
|
||
}
|
||
|
||
// MatchPattern 检查路径是否匹配通配符模式。
|
||
//
|
||
// 支持以下匹配模式:
|
||
// - "*":匹配所有路径
|
||
// - 以 "*" 结尾:前缀匹配(如 "/api/*" 匹配 "/api/xxx")
|
||
// - 以 "/" 结尾:目录前缀匹配
|
||
// - 中间通配符:"/api/*/users" 匹配 "/api/v1/users"
|
||
// - 其他:精确匹配
|
||
func MatchPattern(pattern, path string) bool {
|
||
// 特殊情况:* 匹配所有
|
||
if pattern == "*" {
|
||
return true
|
||
}
|
||
|
||
// 目录前缀匹配(pattern 以 / 结尾)
|
||
if strings.HasSuffix(pattern, "/") {
|
||
return strings.HasPrefix(path, pattern)
|
||
}
|
||
|
||
// 检查是否有通配符
|
||
if !strings.Contains(pattern, "*") {
|
||
return path == pattern
|
||
}
|
||
|
||
// 简单的前缀匹配:/api/users/* 匹配 /api/users/123
|
||
if prefix, ok := strings.CutSuffix(pattern, "*"); ok {
|
||
return strings.HasPrefix(path, prefix)
|
||
}
|
||
|
||
// 中间通配符:/api/*/users 匹配 /api/v1/users
|
||
parts := strings.Split(pattern, "*")
|
||
if len(parts) == 2 {
|
||
return strings.HasPrefix(path, parts[0]) && strings.HasSuffix(path, parts[1])
|
||
}
|
||
|
||
// 复杂模式不支持,返回 false
|
||
return false
|
||
}
|
||
|
||
// PurgeRequest 缓存清理请求结构。
|
||
type PurgeRequest struct {
|
||
// Path 精确路径
|
||
Path string `json:"path,omitempty"`
|
||
|
||
// Pattern 通配符模式(支持 * 通配符)
|
||
Pattern string `json:"pattern,omitempty"`
|
||
|
||
// Method HTTP 方法,默认 "GET"
|
||
Method string `json:"method,omitempty"`
|
||
}
|
||
|
||
// PurgeResponse 缓存清理响应结构。
|
||
type PurgeResponse struct {
|
||
// Deleted 被删除的缓存条目数
|
||
Deleted int `json:"deleted"`
|
||
}
|