lolly/internal/cache/file_cache.go

789 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"`
}