feat(cache): 添加缓存 TTL 新鲜度验证优化

在 FileEntry 中新增 CachedAt 字段记录缓存时间,实现 TTL 窗口内的缓存新鲜度验证:
- TTL 内跳过 ModTime 验证,减少 os.Stat 调用
- TTL 过期后验证 ModTime,文件未修改时刷新 CachedAt
- 默认 TTL 设置为 5 秒

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-15 17:53:51 +08:00
parent b0380f8798
commit fa55bfd497
3 changed files with 77 additions and 1 deletions

View File

@ -27,6 +27,7 @@ import (
// FileEntry 文件缓存条目,存储单个文件的缓存信息。
type FileEntry struct {
ModTime time.Time
CachedAt time.Time // 缓存时间,用于 TTL 验证(新鲜度)
LastAccess time.Time
element *list.Element
Path string
@ -100,6 +101,12 @@ func (c *FileCache) Get(path string) (*FileEntry, bool) {
return nil, false
}
// 迁移处理: CachedAt 为零值时视为刚刚缓存(旧条目)
// 在锁内执行,确保并发安全
if entry.CachedAt.IsZero() {
entry.CachedAt = time.Now()
}
// 更新访问时间并移到 LRU 链表头部
entry.LastAccess = time.Now()
c.lruList.MoveToFront(entry.element)
@ -130,6 +137,7 @@ func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time)
entry.Data = data
entry.Size = size
entry.ModTime = modTime
entry.CachedAt = time.Now() // 更新缓存时间
entry.LastAccess = time.Now()
c.currentSize += size
c.lruList.MoveToFront(entry.element)
@ -143,6 +151,7 @@ func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time)
Data: data,
Size: size,
ModTime: modTime,
CachedAt: time.Now(), // 设置缓存时间
LastAccess: time.Now(),
}
entry.element = c.lruList.PushFront(entry)
@ -168,6 +177,23 @@ func (c *FileCache) Delete(path string) {
}
}
// 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 链表和条目映射中移除指定条目,更新当前内存使用量。

View File

@ -22,6 +22,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/cache"
@ -41,6 +42,7 @@ import (
// - alias 与 root 互斥,同时配置时 alias 优先
type StaticHandler struct {
fileCache *cache.FileCache
cacheTTL time.Duration // 缓存新鲜度 TTL默认 5s0 表示每次验证 ModTime
gzipStatic *compression.GzipStatic
router *Router
root string
@ -202,6 +204,23 @@ func (h *StaticHandler) SetSymlinkCheck(enabled bool) {
h.symlinkCheck = enabled
}
// SetCacheTTL 设置缓存新鲜度 TTL。
//
// TTL 控制缓存条目的新鲜度验证间隔。
// 在 TTL 窗口内,缓存命中时跳过 ModTime 验证以减少 os.Stat 调用。
//
// 参数:
// - ttl: TTL 时间间隔
//
// TTL 值说明:
// - ttl > 0: TTL 内跳过 ModTime 验证,过期后验证
// - ttl = 0: 每次请求验证 ModTime向后兼容
//
// 默认 TTL 为 5 秒。
func (h *StaticHandler) SetCacheTTL(ttl time.Duration) {
h.cacheTTL = ttl
}
// Handle 处理静态文件请求。
//
// 根据请求路径查找并返回对应的静态文件。
@ -456,7 +475,36 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
return
}
// 直接返回文件
// Phase 2: 缓存查找 + TTL 验证
// 在 serveFile 调用前检查缓存,减少 os.ReadFile 调用
// 注意: CachedAt 迁移已在 FileCache.Get() 内部完成,确保并发安全
if h.fileCache != nil {
if entry, ok := h.fileCache.Get(filePath); ok {
// TTL 验证cacheTTL > 0 时启用)
if h.cacheTTL > 0 && time.Since(entry.CachedAt) < h.cacheTTL {
// TTL 内直接返回(无需验证 ModTime
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
return
}
// TTL 过期或未启用 TTL验证文件新鲜度
if entry.ModTime.Equal(info.ModTime()) {
// 文件未修改,刷新 TTL 并返回
if h.cacheTTL > 0 {
h.fileCache.RefreshCachedAt(filePath)
}
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath))
return
}
// 文件已修改,删除缓存继续处理
h.fileCache.Delete(filePath)
}
}
// Phase 3: 缓存未命中,调用 serveFile 处理
h.serveFile(ctx, filePath, info)
}

View File

@ -1115,6 +1115,8 @@ func (s *Server) registerStaticHandlers(router *handler.Router, cfg *config.Serv
)
if s.fileCache != nil {
staticHandler.SetFileCache(s.fileCache)
// 设置默认缓存 TTL (5s)
staticHandler.SetCacheTTL(5 * time.Second)
}
if cfg.Compression.GzipStatic {
staticHandler.SetGzipStatic(true, cfg.Compression.GzipStaticExtensions)