diff --git a/internal/cache/file_cache.go b/internal/cache/file_cache.go index 68f7267..c18e3fd 100644 --- a/internal/cache/file_cache.go +++ b/internal/cache/file_cache.go @@ -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 链表和条目映射中移除指定条目,更新当前内存使用量。 diff --git a/internal/handler/static.go b/internal/handler/static.go index 6cce8fa..8406070 100644 --- a/internal/handler/static.go +++ b/internal/handler/static.go @@ -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) } diff --git a/internal/server/server.go b/internal/server/server.go index 84342bf..c12b695 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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)