Skip to content

Commit e2ebd42

Browse files
committed
feat(cache): enhance disk cache management with concurrency control and cleanup optimizations
1 parent 9ef7740 commit e2ebd42

8 files changed

Lines changed: 128 additions & 84 deletions

File tree

common/disk_cache.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ func CleanupOldDiskCacheFiles(maxAge time.Duration) error {
127127
continue
128128
}
129129
if now.Sub(info.ModTime()) > maxAge {
130-
os.Remove(filepath.Join(dir, entry.Name()))
130+
// 注意:后台清理任务删除文件时,由于无法得知原始 base64Size,
131+
// 只能按磁盘文件大小扣减。这在目前 base64 存储模式下是准确的。
132+
if err := os.Remove(filepath.Join(dir, entry.Name())); err == nil {
133+
DecrementDiskFiles(info.Size())
134+
}
131135
}
132136
}
133137
return nil

common/disk_cache_config.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,12 @@ func IncrementDiskFiles(size int64) {
113113

114114
// DecrementDiskFiles 减少磁盘文件计数
115115
func DecrementDiskFiles(size int64) {
116-
atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1)
117-
atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size)
116+
if atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1) < 0 {
117+
atomic.StoreInt64(&diskCacheStats.ActiveDiskFiles, 0)
118+
}
119+
if atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size) < 0 {
120+
atomic.StoreInt64(&diskCacheStats.CurrentDiskUsageBytes, 0)
121+
}
118122
}
119123

120124
// IncrementMemoryBuffers 增加内存缓存计数

controller/performance.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55
"os"
66
"runtime"
7+
"time"
78

89
"github.com/QuantumNous/new-api/common"
910
"github.com/gin-gonic/gin"
@@ -77,10 +78,8 @@ type PerformanceConfig struct {
7778

7879
// GetPerformanceStats 获取性能统计信息
7980
func GetPerformanceStats(c *gin.Context) {
80-
// 先同步磁盘缓存统计,确保显示准确
81-
common.SyncDiskCacheStats()
82-
83-
// 获取缓存统计
81+
// 不再每次获取统计都全量扫描磁盘,依赖原子计数器保证性能
82+
// 仅在系统启动或显式清理时同步
8483
cacheStats := common.GetDiskCacheStats()
8584

8685
// 获取内存统计
@@ -123,25 +122,19 @@ func GetPerformanceStats(c *gin.Context) {
123122
})
124123
}
125124

126-
// ClearDiskCache 清理磁盘缓存
125+
// ClearDiskCache 清理不活跃的磁盘缓存
127126
func ClearDiskCache(c *gin.Context) {
128-
// 使用统一的缓存目录
129-
dir := common.GetDiskCacheDir()
130-
131-
// 删除缓存目录
132-
err := os.RemoveAll(dir)
133-
if err != nil && !os.IsNotExist(err) {
127+
// 清理超过 10 分钟未使用的缓存文件
128+
// 10 分钟是一个安全的阈值,确保正在进行的请求不会被误删
129+
err := common.CleanupOldDiskCacheFiles(10 * time.Minute)
130+
if err != nil {
134131
common.ApiError(c, err)
135132
return
136133
}
137134

138-
// 重置统计(包括命中次数和使用量)
139-
common.ResetDiskCacheStats()
140-
common.ResetDiskCacheUsage()
141-
142135
c.JSON(http.StatusOK, gin.H{
143136
"success": true,
144-
"message": "磁盘缓存已清理",
137+
"message": "不活跃的磁盘缓存已清理",
145138
})
146139
}
147140

service/file_service.go

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,44 @@ func LoadFileSource(c *gin.Context, source *types.FileSource, reason ...string)
3636
return nil, fmt.Errorf("file source is nil")
3737
}
3838

39-
// 如果已有缓存,直接返回
39+
if common.DebugEnabled {
40+
logger.LogDebug(c, fmt.Sprintf("LoadFileSource starting for: %s", source.GetIdentifier()))
41+
}
42+
43+
// 1. 快速检查内部缓存
4044
if source.HasCache() {
45+
// 即使命中内部缓存,也要确保注册到清理列表(如果尚未注册)
46+
if c != nil {
47+
registerSourceForCleanup(c, source)
48+
}
4149
return source.GetCache(), nil
4250
}
4351

52+
// 2. 加锁保护加载过程
53+
source.Mu().Lock()
54+
defer source.Mu().Unlock()
55+
56+
// 3. 双重检查
57+
if source.HasCache() {
58+
if c != nil {
59+
registerSourceForCleanup(c, source)
60+
}
61+
return source.GetCache(), nil
62+
}
63+
64+
// 4. 如果是 URL,检查 Context 缓存
65+
var contextKey string
66+
if source.IsURL() && c != nil {
67+
contextKey = getContextCacheKey(source.URL)
68+
if cachedData, exists := c.Get(contextKey); exists {
69+
data := cachedData.(*types.CachedFileData)
70+
source.SetCache(data)
71+
registerSourceForCleanup(c, source)
72+
return data, nil
73+
}
74+
}
75+
76+
// 5. 执行加载逻辑
4477
var cachedData *types.CachedFileData
4578
var err error
4679

@@ -54,10 +87,13 @@ func LoadFileSource(c *gin.Context, source *types.FileSource, reason ...string)
5487
return nil, err
5588
}
5689

57-
// 设置缓存
90+
// 6. 设置缓存
5891
source.SetCache(cachedData)
92+
if contextKey != "" && c != nil {
93+
c.Set(contextKey, cachedData)
94+
}
5995

60-
// 注册到 context 以便请求结束时自动清理
96+
// 7. 注册到 context 以便请求结束时自动清理
6197
if c != nil {
6298
registerSourceForCleanup(c, source)
6399
}
@@ -67,13 +103,18 @@ func LoadFileSource(c *gin.Context, source *types.FileSource, reason ...string)
67103

68104
// registerSourceForCleanup 注册 FileSource 到 context 以便请求结束时清理
69105
func registerSourceForCleanup(c *gin.Context, source *types.FileSource) {
106+
if source.IsRegistered() {
107+
return
108+
}
109+
70110
key := string(constant.ContextKeyFileSourcesToCleanup)
71111
var sources []*types.FileSource
72112
if existing, exists := c.Get(key); exists {
73113
sources = existing.([]*types.FileSource)
74114
}
75115
sources = append(sources, source)
76116
c.Set(key, sources)
117+
source.SetRegistered(true)
77118
}
78119

79120
// CleanupFileSources 清理请求中所有注册的 FileSource
@@ -83,9 +124,6 @@ func CleanupFileSources(c *gin.Context) {
83124
if sources, exists := c.Get(key); exists {
84125
for _, source := range sources.([]*types.FileSource) {
85126
if cache := source.GetCache(); cache != nil {
86-
if cache.IsDisk() {
87-
common.DecrementDiskFiles(cache.Size)
88-
}
89127
cache.Close()
90128
}
91129
}
@@ -94,21 +132,13 @@ func CleanupFileSources(c *gin.Context) {
94132
}
95133

96134
// loadFromURL 从 URL 加载文件
97-
// 支持磁盘缓存:当文件大小超过阈值且磁盘缓存可用时,将数据存储到磁盘
98135
func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFileData, error) {
99-
contextKey := getContextCacheKey(url)
100-
101-
// 检查 context 缓存
102-
if cachedData, exists := c.Get(contextKey); exists {
103-
if common.DebugEnabled {
104-
logger.LogDebug(c, fmt.Sprintf("Using cached file data for URL: %s", url))
105-
}
106-
return cachedData.(*types.CachedFileData), nil
107-
}
108-
109136
// 下载文件
110137
var maxFileSize = constant.MaxFileDownloadMB * 1024 * 1024
111138

139+
if common.DebugEnabled {
140+
logger.LogDebug(c, "loadFromURL: initiating download")
141+
}
112142
resp, err := DoDownloadRequest(url, reason...)
113143
if err != nil {
114144
return nil, fmt.Errorf("failed to download file from %s: %w", url, err)
@@ -120,6 +150,9 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
120150
}
121151

122152
// 读取文件内容(限制大小)
153+
if common.DebugEnabled {
154+
logger.LogDebug(c, "loadFromURL: reading response body")
155+
}
123156
fileBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxFileSize+1)))
124157
if err != nil {
125158
return nil, fmt.Errorf("failed to read file content: %w", err)
@@ -147,6 +180,10 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
147180
cachedData = types.NewMemoryCachedData(base64Data, mimeType, int64(len(fileBytes)))
148181
} else {
149182
cachedData = types.NewDiskCachedData(diskPath, mimeType, int64(len(fileBytes)))
183+
cachedData.DiskSize = base64Size
184+
cachedData.OnClose = func(size int64) {
185+
common.DecrementDiskFiles(size)
186+
}
150187
common.IncrementDiskFiles(base64Size)
151188
if common.DebugEnabled {
152189
logger.LogDebug(c, fmt.Sprintf("File cached to disk: %s, size: %d bytes", diskPath, base64Size))
@@ -159,6 +196,9 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
159196

160197
// 如果是图片,尝试获取图片配置
161198
if strings.HasPrefix(mimeType, "image/") {
199+
if common.DebugEnabled {
200+
logger.LogDebug(c, "loadFromURL: decoding image config")
201+
}
162202
config, format, err := decodeImageConfig(fileBytes)
163203
if err == nil {
164204
cachedData.ImageConfig = &config
@@ -170,9 +210,6 @@ func loadFromURL(c *gin.Context, url string, reason ...string) (*types.CachedFil
170210
}
171211
}
172212

173-
// 存入 context 缓存
174-
c.Set(contextKey, cachedData)
175-
176213
return cachedData, nil
177214
}
178215

@@ -187,7 +224,6 @@ func writeToDiskCache(base64Data string) (string, error) {
187224
}
188225

189226
// smartDetectMimeType 智能检测 MIME 类型
190-
// 优先级:Content-Type header > Content-Disposition filename > URL 路径 > 内容嗅探 > 图片解码
191227
func smartDetectMimeType(resp *http.Response, url string, fileBytes []byte) string {
192228
// 1. 尝试从 Content-Type header 获取
193229
mimeType := resp.Header.Get("Content-Type")
@@ -259,13 +295,11 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached
259295

260296
// 处理 data: 前缀
261297
if strings.HasPrefix(base64String, "data:") {
262-
// 格式: data:mime/type;base64,xxxxx
263298
idx := strings.Index(base64String, ",")
264299
if idx != -1 {
265300
header := base64String[:idx]
266301
cleanBase64 = base64String[idx+1:]
267302

268-
// 从 header 提取 MIME 类型
269303
if strings.Contains(header, ":") && strings.Contains(header, ";") {
270304
mimeStart := strings.Index(header, ":") + 1
271305
mimeEnd := strings.Index(header, ";")
@@ -280,36 +314,34 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached
280314
cleanBase64 = base64String
281315
}
282316

283-
// 使用提供的 MIME 类型(如果有)
284317
if providedMimeType != "" {
285318
mimeType = providedMimeType
286319
}
287320

288-
// 解码 base64
289321
decodedData, err := base64.StdEncoding.DecodeString(cleanBase64)
290322
if err != nil {
291323
return nil, fmt.Errorf("failed to decode base64 data: %w", err)
292324
}
293325

294-
// 判断是否使用磁盘缓存(对于 base64 内联数据也支持磁盘缓存)
295326
base64Size := int64(len(cleanBase64))
296327
var cachedData *types.CachedFileData
297328

298329
if shouldUseDiskCache(base64Size) {
299-
// 使用磁盘缓存
300330
diskPath, err := writeToDiskCache(cleanBase64)
301331
if err != nil {
302-
// 磁盘缓存失败,回退到内存
303332
cachedData = types.NewMemoryCachedData(cleanBase64, mimeType, int64(len(decodedData)))
304333
} else {
305334
cachedData = types.NewDiskCachedData(diskPath, mimeType, int64(len(decodedData)))
335+
cachedData.DiskSize = base64Size
336+
cachedData.OnClose = func(size int64) {
337+
common.DecrementDiskFiles(size)
338+
}
306339
common.IncrementDiskFiles(base64Size)
307340
}
308341
} else {
309342
cachedData = types.NewMemoryCachedData(cleanBase64, mimeType, int64(len(decodedData)))
310343
}
311344

312-
// 如果是图片或 MIME 类型未知,尝试解码图片获取更多信息
313345
if mimeType == "" || strings.HasPrefix(mimeType, "image/") {
314346
config, format, err := decodeImageConfig(decodedData)
315347
if err == nil {
@@ -324,8 +356,7 @@ func loadFromBase64(base64String string, providedMimeType string) (*types.Cached
324356
return cachedData, nil
325357
}
326358

327-
// GetImageConfig 获取图片配置(宽高等信息)
328-
// 会自动处理缓存,避免重复下载/解码
359+
// GetImageConfig 获取图片配置
329360
func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, string, error) {
330361
cachedData, err := LoadFileSource(c, source, "get_image_config")
331362
if err != nil {
@@ -336,7 +367,6 @@ func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, str
336367
return *cachedData.ImageConfig, cachedData.ImageFormat, nil
337368
}
338369

339-
// 如果缓存中没有图片配置,尝试解码
340370
base64Str, err := cachedData.GetBase64Data()
341371
if err != nil {
342372
return image.Config{}, "", fmt.Errorf("failed to get base64 data: %w", err)
@@ -351,16 +381,13 @@ func GetImageConfig(c *gin.Context, source *types.FileSource) (image.Config, str
351381
return image.Config{}, "", err
352382
}
353383

354-
// 更新缓存
355384
cachedData.ImageConfig = &config
356385
cachedData.ImageFormat = format
357386

358387
return config, format, nil
359388
}
360389

361390
// GetBase64Data 获取 base64 编码的数据
362-
// 会自动处理缓存,避免重复下载
363-
// 支持内存缓存和磁盘缓存
364391
func GetBase64Data(c *gin.Context, source *types.FileSource, reason ...string) (string, string, error) {
365392
cachedData, err := LoadFileSource(c, source, reason...)
366393
if err != nil {
@@ -375,28 +402,25 @@ func GetBase64Data(c *gin.Context, source *types.FileSource, reason ...string) (
375402

376403
// GetMimeType 获取文件的 MIME 类型
377404
func GetMimeType(c *gin.Context, source *types.FileSource) (string, error) {
378-
// 如果已经有缓存,直接返回
379405
if source.HasCache() {
380406
return source.GetCache().MimeType, nil
381407
}
382408

383-
// 如果是 URL,尝试只获取 header 而不下载完整文件
384409
if source.IsURL() {
385410
mimeType, err := GetFileTypeFromUrl(c, source.URL, "get_mime_type")
386411
if err == nil && mimeType != "" && mimeType != "application/octet-stream" {
387412
return mimeType, nil
388413
}
389414
}
390415

391-
// 否则加载完整数据
392416
cachedData, err := LoadFileSource(c, source, "get_mime_type")
393417
if err != nil {
394418
return "", err
395419
}
396420
return cachedData.MimeType, nil
397421
}
398422

399-
// DetectFileType 检测文件类型(image/audio/video/file)
423+
// DetectFileType 检测文件类型
400424
func DetectFileType(mimeType string) types.FileType {
401425
if strings.HasPrefix(mimeType, "image/") {
402426
return types.FileTypeImage
@@ -414,13 +438,11 @@ func DetectFileType(mimeType string) types.FileType {
414438
func decodeImageConfig(data []byte) (image.Config, string, error) {
415439
reader := bytes.NewReader(data)
416440

417-
// 尝试标准格式
418441
config, format, err := image.DecodeConfig(reader)
419442
if err == nil {
420443
return config, format, nil
421444
}
422445

423-
// 尝试 webp
424446
reader.Seek(0, io.SeekStart)
425447
config, err = webp.DecodeConfig(reader)
426448
if err == nil {
@@ -432,13 +454,11 @@ func decodeImageConfig(data []byte) (image.Config, string, error) {
432454

433455
// guessMimeTypeFromURL 从 URL 猜测 MIME 类型
434456
func guessMimeTypeFromURL(url string) string {
435-
// 移除查询参数
436457
cleanedURL := url
437458
if q := strings.Index(cleanedURL, "?"); q != -1 {
438459
cleanedURL = cleanedURL[:q]
439460
}
440461

441-
// 获取最后一段
442462
if slash := strings.LastIndex(cleanedURL, "/"); slash != -1 && slash+1 < len(cleanedURL) {
443463
last := cleanedURL[slash+1:]
444464
if dot := strings.LastIndex(last, "."); dot != -1 && dot+1 < len(last) {

0 commit comments

Comments
 (0)