lolly/internal/cache/disk_cache.go
xfy 91d67ad384 refactor(cache,config): 代码清理和优化
- disk_cache: 忽略 filepath.Walk 和 Delete 返回值
- tiered_cache: 忽略 l1.Delete 返回值,删除未使用的 revalidate 函数
- config: 简化 multiplier 变量声明

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 13:34:51 +08:00

416 lines
9.2 KiB
Go
Raw 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 提供文件缓存和代理缓存功能。
//
// 该文件实现 DiskCache 磁盘缓存后端,支持:
// - 目录层级配置levels=1:2
// - 原子写入策略(.tmp → .data
// - 懒加载(后台加载元数据,不阻塞启动)
// - CRC32 校验和验证数据完整性
//
// 主要用途:
//
// 作为 L2 缓存层,持久化代理响应到磁盘,支持服务重启后恢复。
//
// 作者xfy
package cache
import (
"encoding/json"
"fmt"
"hash/crc32"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
)
// DiskCacheConfig 磁盘缓存配置。
type DiskCacheConfig struct {
// Path 缓存根目录
Path string
// Levels 目录层级,如 "1:2" 表示两级目录
Levels string
// MaxSize 最大缓存大小(字节)
MaxSize int64
// Inactive 未访问淘汰时间
Inactive time.Duration
}
// DiskCache 磁盘缓存实现。
type DiskCache struct {
basePath string
levels []int
maxSize int64
inactive time.Duration
currentSize atomic.Int64
entries map[uint64]*DiskCacheMeta
mu sync.RWMutex
stopCh chan struct{}
// 懒加载相关
loaded atomic.Bool
loadCh chan struct{}
// 统计
hitCount atomic.Int64
missCount atomic.Int64
evictions atomic.Int64
}
// DiskCacheMeta 磁盘缓存元数据。
type DiskCacheMeta struct {
HashKey uint64 `json:"hash_key"`
OrigKey string `json:"orig_key"`
Created time.Time `json:"created"`
MaxAge time.Duration `json:"max_age"`
Status int `json:"status"`
Size int64 `json:"size"`
Headers map[string]string `json:"headers,omitempty"`
CRC32 uint32 `json:"crc32"`
}
// DiskCacheEntry 磁盘缓存条目(包含数据)。
type DiskCacheEntry struct {
*DiskCacheMeta
Data []byte
}
// NewDiskCache 创建磁盘缓存实例(懒加载模式)。
func NewDiskCache(cfg *DiskCacheConfig) (*DiskCache, error) {
if cfg.Path == "" {
return nil, fmt.Errorf("disk cache path is required")
}
// 确保目录存在
if err := os.MkdirAll(cfg.Path, 0o755); err != nil {
return nil, fmt.Errorf("create cache directory: %w", err)
}
dc := &DiskCache{
basePath: cfg.Path,
levels: parseLevels(cfg.Levels),
maxSize: cfg.MaxSize,
inactive: cfg.Inactive,
entries: make(map[uint64]*DiskCacheMeta),
loadCh: make(chan struct{}),
stopCh: make(chan struct{}),
}
// 启动后台加载,不阻塞服务启动
go dc.lazyLoad()
return dc, nil
}
// parseLevels 解析目录层级配置。
// 支持格式:""(无层级)、"1"(一级)、"1:2"(两级)
func parseLevels(levels string) []int {
if levels == "" {
return nil
}
var result []int
start := 0
for i := 0; i <= len(levels); i++ {
if i == len(levels) || levels[i] == ':' {
var level int
for j := start; j < i; j++ {
level = level*10 + int(levels[j]-'0')
}
if level > 0 {
result = append(result, level)
}
start = i + 1
}
}
return result
}
// lazyLoad 后台加载缓存元数据。
func (dc *DiskCache) lazyLoad() {
defer close(dc.loadCh)
// 扫描目录加载元数据(不加载实际数据)
_ = filepath.Walk(dc.basePath, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
// 只处理 .meta 文件
if filepath.Ext(path) != ".meta" {
return nil
}
meta := dc.loadMetaFile(path)
if meta != nil {
dc.mu.Lock()
dc.entries[meta.HashKey] = meta
dc.mu.Unlock()
dc.currentSize.Add(meta.Size)
}
return nil
})
dc.loaded.Store(true)
}
// loadMetaFile 加载元数据文件。
func (dc *DiskCache) loadMetaFile(path string) *DiskCacheMeta {
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var meta DiskCacheMeta
if err := json.Unmarshal(data, &meta); err != nil {
return nil
}
return &meta
}
// Get 获取缓存条目(实现 CacheBackend 接口)。
func (dc *DiskCache) Get(hashKey uint64, origKey string) (*ProxyCacheEntry, bool, bool) {
// 如果未完成加载,等待加载完成或超时
if !dc.loaded.Load() {
select {
case <-dc.loadCh:
// 加载完成,继续
case <-time.After(100 * time.Millisecond):
// 超时,返回未命中(避免阻塞请求)
dc.missCount.Add(1)
return nil, false, false
}
}
dc.mu.RLock()
meta, exists := dc.entries[hashKey]
dc.mu.RUnlock()
if !exists {
dc.missCount.Add(1)
return nil, false, false
}
// 双重验证:检查原始 key 是否匹配
if meta.OrigKey != origKey {
dc.missCount.Add(1)
return nil, false, false
}
// 读取数据文件
dataPath := dc.filePathFromHash(hashKey, "data")
data, err := os.ReadFile(dataPath)
if err != nil {
dc.missCount.Add(1)
return nil, false, false
}
// 验证 CRC32
if meta.CRC32 != 0 {
if crc32.ChecksumIEEE(data) != meta.CRC32 {
// 数据损坏,删除条目
_ = dc.Delete(hashKey)
dc.missCount.Add(1)
return nil, false, false
}
}
// 检查是否过期
now := time.Now()
expiresAt := meta.Created.Add(meta.MaxAge)
stale := now.After(expiresAt)
dc.hitCount.Add(1)
// 转换为 ProxyCacheEntry
entry := &ProxyCacheEntry{
Key: meta.OrigKey,
OrigKey: meta.OrigKey,
Data: data,
Headers: meta.Headers,
Status: meta.Status,
Created: meta.Created,
MaxAge: meta.MaxAge,
}
return entry, true, stale
}
// Set 设置缓存条目(实现 CacheBackend 接口)。
func (dc *DiskCache) Set(hashKey uint64, origKey string, data []byte, headers map[string]string, status int, maxAge time.Duration) {
// 计算文件路径
dataPath := dc.filePathFromHash(hashKey, "data")
metaPath := dc.filePathFromHash(hashKey, "meta")
// 确保目录存在
dir := filepath.Dir(dataPath)
if err := os.MkdirAll(dir, 0o755); err != nil {
return
}
// 计算 CRC32
crc := crc32.ChecksumIEEE(data)
// 创建元数据
meta := &DiskCacheMeta{
HashKey: hashKey,
OrigKey: origKey,
Created: time.Now(),
MaxAge: maxAge,
Status: status,
Size: int64(len(data)),
Headers: headers,
CRC32: crc,
}
// 原子写入数据文件:先写临时文件,再重命名
tmpDataPath := dataPath + ".tmp"
if err := os.WriteFile(tmpDataPath, data, 0o644); err != nil {
return
}
if err := os.Rename(tmpDataPath, dataPath); err != nil {
os.Remove(tmpDataPath)
return
}
// 写入元数据文件
metaData, err := json.Marshal(meta)
if err != nil {
return
}
tmpMetaPath := metaPath + ".tmp"
if err := os.WriteFile(tmpMetaPath, metaData, 0o644); err != nil {
return
}
if err := os.Rename(tmpMetaPath, metaPath); err != nil {
os.Remove(tmpMetaPath)
return
}
// 更新内存索引
dc.mu.Lock()
oldMeta, existed := dc.entries[hashKey]
dc.entries[hashKey] = meta
dc.mu.Unlock()
// 更新大小统计
if existed && oldMeta != nil {
dc.currentSize.Add(-oldMeta.Size)
}
dc.currentSize.Add(meta.Size)
// 检查是否需要淘汰
if dc.maxSize > 0 && dc.currentSize.Load() > dc.maxSize {
go dc.evict()
}
}
// Delete 删除缓存条目(实现 CacheBackend 接口)。
func (dc *DiskCache) Delete(hashKey uint64) error {
dc.mu.Lock()
meta, exists := dc.entries[hashKey]
if exists {
delete(dc.entries, hashKey)
dc.currentSize.Add(-meta.Size)
dc.evictions.Add(1)
}
dc.mu.Unlock()
if !exists {
return nil
}
// 删除文件
dataPath := dc.filePathFromHash(hashKey, "data")
metaPath := dc.filePathFromHash(hashKey, "meta")
os.Remove(dataPath)
os.Remove(metaPath)
return nil
}
// CacheStats 返回缓存统计信息(实现 CacheBackend 接口)。
func (dc *DiskCache) CacheStats() CacheStats {
dc.mu.RLock()
entries := int64(len(dc.entries))
dc.mu.RUnlock()
return CacheStats{
Entries: entries,
Size: dc.currentSize.Load(),
HitCount: dc.hitCount.Load(),
MissCount: dc.missCount.Load(),
Evictions: dc.evictions.Load(),
}
}
// Stop 停止磁盘缓存。
func (dc *DiskCache) Stop() {
close(dc.stopCh)
}
// filePathFromHash 根据哈希值计算文件路径。
func (dc *DiskCache) filePathFromHash(hashKey uint64, ext string) string {
// 将哈希值转换为十六进制字符串
hashStr := fmt.Sprintf("%016x", hashKey)
if len(dc.levels) == 0 {
return filepath.Join(dc.basePath, hashStr+"."+ext)
}
// 根据层级构建路径
parts := []string{dc.basePath}
offset := len(hashStr)
for _, level := range dc.levels {
if offset < level {
break
}
offset -= level
parts = append(parts, hashStr[offset:offset+level])
}
parts = append(parts, hashStr+"."+ext)
return filepath.Join(parts...)
}
// evict 淘汰旧条目。
func (dc *DiskCache) evict() {
// 简单策略:删除最旧的条目直到大小低于阈值
targetSize := dc.maxSize * 9 / 10 // 淘汰到 90%
for dc.currentSize.Load() > targetSize {
dc.mu.Lock()
// 找到最旧的条目
var oldestKey uint64
var oldestTime time.Time
for key, meta := range dc.entries {
if oldestKey == 0 || meta.Created.Before(oldestTime) {
oldestKey = key
oldestTime = meta.Created
}
}
if oldestKey == 0 {
dc.mu.Unlock()
break
}
meta := dc.entries[oldestKey]
delete(dc.entries, oldestKey)
dc.currentSize.Add(-meta.Size)
dc.evictions.Add(1)
dc.mu.Unlock()
// 删除文件
dataPath := dc.filePathFromHash(oldestKey, "data")
metaPath := dc.filePathFromHash(oldestKey, "meta")
os.Remove(dataPath)
os.Remove(metaPath)
}
}