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:
parent
b0380f8798
commit
fa55bfd497
26
internal/cache/file_cache.go
vendored
26
internal/cache/file_cache.go
vendored
@ -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 链表和条目映射中移除指定条目,更新当前内存使用量。
|
||||
|
||||
@ -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(默认 5s,0 表示每次验证 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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user