Commit a8ff74c2 by CaIon

feat(cache): enhance disk cache management with concurrency control and cleanup optimizations

parent 9ef9e788
...@@ -127,7 +127,11 @@ func CleanupOldDiskCacheFiles(maxAge time.Duration) error { ...@@ -127,7 +127,11 @@ func CleanupOldDiskCacheFiles(maxAge time.Duration) error {
continue continue
} }
if now.Sub(info.ModTime()) > maxAge { if now.Sub(info.ModTime()) > maxAge {
os.Remove(filepath.Join(dir, entry.Name())) // 注意:后台清理任务删除文件时,由于无法得知原始 base64Size,
// 只能按磁盘文件大小扣减。这在目前 base64 存储模式下是准确的。
if err := os.Remove(filepath.Join(dir, entry.Name())); err == nil {
DecrementDiskFiles(info.Size())
}
} }
} }
return nil return nil
......
...@@ -113,8 +113,12 @@ func IncrementDiskFiles(size int64) { ...@@ -113,8 +113,12 @@ func IncrementDiskFiles(size int64) {
// DecrementDiskFiles 减少磁盘文件计数 // DecrementDiskFiles 减少磁盘文件计数
func DecrementDiskFiles(size int64) { func DecrementDiskFiles(size int64) {
atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1) if atomic.AddInt64(&diskCacheStats.ActiveDiskFiles, -1) < 0 {
atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size) atomic.StoreInt64(&diskCacheStats.ActiveDiskFiles, 0)
}
if atomic.AddInt64(&diskCacheStats.CurrentDiskUsageBytes, -size) < 0 {
atomic.StoreInt64(&diskCacheStats.CurrentDiskUsageBytes, 0)
}
} }
// IncrementMemoryBuffers 增加内存缓存计数 // IncrementMemoryBuffers 增加内存缓存计数
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"time"
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -77,10 +78,8 @@ type PerformanceConfig struct { ...@@ -77,10 +78,8 @@ type PerformanceConfig struct {
// GetPerformanceStats 获取性能统计信息 // GetPerformanceStats 获取性能统计信息
func GetPerformanceStats(c *gin.Context) { func GetPerformanceStats(c *gin.Context) {
// 先同步磁盘缓存统计,确保显示准确 // 不再每次获取统计都全量扫描磁盘,依赖原子计数器保证性能
common.SyncDiskCacheStats() // 仅在系统启动或显式清理时同步
// 获取缓存统计
cacheStats := common.GetDiskCacheStats() cacheStats := common.GetDiskCacheStats()
// 获取内存统计 // 获取内存统计
...@@ -123,25 +122,19 @@ func GetPerformanceStats(c *gin.Context) { ...@@ -123,25 +122,19 @@ func GetPerformanceStats(c *gin.Context) {
}) })
} }
// ClearDiskCache 清理磁盘缓存 // ClearDiskCache 清理不活跃的磁盘缓存
func ClearDiskCache(c *gin.Context) { func ClearDiskCache(c *gin.Context) {
// 使用统一的缓存目录 // 清理超过 10 分钟未使用的缓存文件
dir := common.GetDiskCacheDir() // 10 分钟是一个安全的阈值,确保正在进行的请求不会被误删
err := common.CleanupOldDiskCacheFiles(10 * time.Minute)
// 删除缓存目录 if err != nil {
err := os.RemoveAll(dir)
if err != nil && !os.IsNotExist(err) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
// 重置统计(包括命中次数和使用量)
common.ResetDiskCacheStats()
common.ResetDiskCacheUsage()
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "磁盘缓存已清理", "message": "不活跃的磁盘缓存已清理",
}) })
} }
......
...@@ -258,16 +258,18 @@ func EstimateRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *rela ...@@ -258,16 +258,18 @@ func EstimateRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *rela
// 如果文件类型未知且需要获取,通过 MIME 类型检测 // 如果文件类型未知且需要获取,通过 MIME 类型检测
if file.FileType == "" || (file.Source.IsURL() && shouldFetchFiles) { if file.FileType == "" || (file.Source.IsURL() && shouldFetchFiles) {
mimeType, err := GetMimeType(c, file.Source) // 注意:这里我们直接调用 LoadFileSource 而不是 GetMimeType
// 因为 GetMimeType 内部可能会调用 GetFileTypeFromUrl (HEAD 请求)
// 而我们这里既然要计算 token,通常需要完整数据
cachedData, err := LoadFileSource(c, file.Source, "token_counter")
if err != nil { if err != nil {
if shouldFetchFiles { if shouldFetchFiles {
return 0, fmt.Errorf("error getting file type: %v", err) return 0, fmt.Errorf("error getting file type: %v", err)
} }
// 如果不需要获取,使用默认类型
continue continue
} }
file.MimeType = mimeType file.MimeType = cachedData.MimeType
file.FileType = DetectFileType(mimeType) file.FileType = DetectFileType(cachedData.MimeType)
} }
} }
......
...@@ -25,8 +25,14 @@ type FileSource struct { ...@@ -25,8 +25,14 @@ type FileSource struct {
// 内部缓存(不导出,不序列化) // 内部缓存(不导出,不序列化)
cachedData *CachedFileData cachedData *CachedFileData
cacheMu sync.RWMutex
cacheLoaded bool cacheLoaded bool
registered bool // 是否已注册到清理列表
mu sync.Mutex // 保护加载过程
}
// Mu 获取内部锁
func (f *FileSource) Mu() *sync.Mutex {
return &f.mu
} }
// CachedFileData 缓存的文件数据 // CachedFileData 缓存的文件数据
...@@ -35,14 +41,19 @@ type CachedFileData struct { ...@@ -35,14 +41,19 @@ type CachedFileData struct {
base64Data string // 内存中的 base64 数据(小文件) base64Data string // 内存中的 base64 数据(小文件)
MimeType string // MIME 类型 MimeType string // MIME 类型
Size int64 // 文件大小(字节) Size int64 // 文件大小(字节)
DiskSize int64 // 磁盘缓存实际占用大小(字节,通常是 base64 长度)
ImageConfig *image.Config // 图片配置(如果是图片) ImageConfig *image.Config // 图片配置(如果是图片)
ImageFormat string // 图片格式(如果是图片) ImageFormat string // 图片格式(如果是图片)
// 磁盘缓存相关 // 磁盘缓存相关
diskPath string // 磁盘缓存文件路径(大文件) diskPath string // 磁盘缓存文件路径(大文件)
isDisk bool // 是否使用磁盘缓存 isDisk bool // 是否使用磁盘缓存
diskMu sync.Mutex // 磁盘操作锁 diskMu sync.Mutex // 磁盘操作锁(保护磁盘文件的读取和删除)
diskClosed bool // 是否已关闭/清理 diskClosed bool // 是否已关闭/清理
statDecremented bool // 是否已扣减统计
// 统计回调,避免循环依赖
OnClose func(size int64)
} }
// NewMemoryCachedData 创建内存缓存的数据 // NewMemoryCachedData 创建内存缓存的数据
...@@ -114,7 +125,13 @@ func (c *CachedFileData) Close() error { ...@@ -114,7 +125,13 @@ func (c *CachedFileData) Close() error {
c.diskClosed = true c.diskClosed = true
if c.diskPath != "" { if c.diskPath != "" {
return os.Remove(c.diskPath) err := os.Remove(c.diskPath)
// 只有在删除成功且未扣减过统计时,才执行回调
if err == nil && !c.statDecremented && c.OnClose != nil {
c.OnClose(c.DiskSize)
c.statDecremented = true
}
return err
} }
return nil return nil
} }
...@@ -170,31 +187,32 @@ func (f *FileSource) GetRawData() string { ...@@ -170,31 +187,32 @@ func (f *FileSource) GetRawData() string {
// SetCache 设置缓存数据 // SetCache 设置缓存数据
func (f *FileSource) SetCache(data *CachedFileData) { func (f *FileSource) SetCache(data *CachedFileData) {
f.cacheMu.Lock()
defer f.cacheMu.Unlock()
f.cachedData = data f.cachedData = data
f.cacheLoaded = true f.cacheLoaded = true
} }
// IsRegistered 是否已注册到清理列表
func (f *FileSource) IsRegistered() bool {
return f.registered
}
// SetRegistered 设置注册状态
func (f *FileSource) SetRegistered(registered bool) {
f.registered = registered
}
// GetCache 获取缓存数据 // GetCache 获取缓存数据
func (f *FileSource) GetCache() *CachedFileData { func (f *FileSource) GetCache() *CachedFileData {
f.cacheMu.RLock()
defer f.cacheMu.RUnlock()
return f.cachedData return f.cachedData
} }
// HasCache 是否有缓存 // HasCache 是否有缓存
func (f *FileSource) HasCache() bool { func (f *FileSource) HasCache() bool {
f.cacheMu.RLock()
defer f.cacheMu.RUnlock()
return f.cacheLoaded && f.cachedData != nil return f.cacheLoaded && f.cachedData != nil
} }
// ClearCache 清除缓存,释放内存和磁盘文件 // ClearCache 清除缓存,释放内存和磁盘文件
func (f *FileSource) ClearCache() { func (f *FileSource) ClearCache() {
f.cacheMu.Lock()
defer f.cacheMu.Unlock()
// 如果有缓存数据,先关闭它(会清理磁盘文件) // 如果有缓存数据,先关闭它(会清理磁盘文件)
if f.cachedData != nil { if f.cachedData != nil {
f.cachedData.Close() f.cachedData.Close()
......
...@@ -442,6 +442,9 @@ ...@@ -442,6 +442,9 @@
"兑换人ID": "兑换人ID", "兑换人ID": "兑换人ID",
"兑换成功!": "兑换成功!", "兑换成功!": "兑换成功!",
"兑换码充值": "兑换码充值", "兑换码充值": "兑换码充值",
"确认清理不活跃的磁盘缓存?": "确认清理不活跃的磁盘缓存?",
"这将删除超过 10 分钟未使用的临时缓存文件": "这将删除超过 10 分钟未使用的临时缓存文件",
"清理不活跃缓存": "清理不活跃缓存",
"兑换码创建成功": "兑换码创建成功", "兑换码创建成功": "兑换码创建成功",
"兑换码创建成功,是否下载兑换码?": "兑换码创建成功,是否下载兑换码?", "兑换码创建成功,是否下载兑换码?": "兑换码创建成功,是否下载兑换码?",
"兑换码创建成功!": "兑换码创建成功!", "兑换码创建成功!": "兑换码创建成功!",
......
...@@ -291,11 +291,11 @@ export default function SettingsPerformance(props) { ...@@ -291,11 +291,11 @@ export default function SettingsPerformance(props) {
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<Button onClick={fetchStats}>{t('刷新统计')}</Button> <Button onClick={fetchStats}>{t('刷新统计')}</Button>
<Popconfirm <Popconfirm
title={t('确认清理磁盘缓存?')} title={t('确认清理不活跃的磁盘缓存?')}
content={t('这将删除所有临时缓存文件')} content={t('这将删除超过 10 分钟未使用的临时缓存文件')}
onConfirm={clearDiskCache} onConfirm={clearDiskCache}
> >
<Button type='warning'>{t('清理磁盘缓存')}</Button> <Button type='warning'>{t('清理不活跃缓存')}</Button>
</Popconfirm> </Popconfirm>
<Button onClick={resetStats}>{t('重置统计')}</Button> <Button onClick={resetStats}>{t('重置统计')}</Button>
<Button onClick={forceGC}>{t('执行 GC')}</Button> <Button onClick={forceGC}>{t('执行 GC')}</Button>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment