Files
VptPassiveAdapter/fs/file_list_cache.go
Jerry Yan 686401162f feat(config): 添加文件列表缓存配置并优化阿里云和S3适配器缓存实现
- 添加 CacheConfig 结构体定义文件列表缓存的TTL和最大条目数
- 在RecordConfig中集成Cache配置项
- 为AliOSS和S3适配器实现统一的文件列表缓存机制
- 移除原有的sync.Map缓存实现和定时清理逻辑
- 引入go-cache依赖库实现专业的缓存管理功能
- 使用LRU算法控制缓存大小避免内存泄漏
- 通过singleflight实现缓存穿透保护和并发控制
- 更新配置文件添加缓存相关配置项
- 在.gitignore中添加.exe文件忽略规则
2025-12-29 11:17:18 +08:00

183 lines
3.9 KiB
Go

package fs
import (
"ZhenTuLocalPassiveAdapter/config"
"ZhenTuLocalPassiveAdapter/dto"
"container/list"
"fmt"
"sync"
"time"
"github.com/patrickmn/go-cache"
"golang.org/x/sync/singleflight"
)
const (
defaultFileListCacheTTLSeconds = 30
defaultFileListCacheMaxEntries = 256
fileListCacheCleanupInterval = 1 * time.Minute
)
var (
s3FileListCacheOnce sync.Once
s3FileListCacheInstance *fileListCache
aliOssFileListCacheOnce sync.Once
aliOssFileListCacheInstance *fileListCache
)
func getS3FileListCache() *fileListCache {
s3FileListCacheOnce.Do(func() {
s3FileListCacheInstance = newFileListCache(getFileListCacheTTL(), getFileListCacheMaxEntries())
})
return s3FileListCacheInstance
}
func getAliOssFileListCache() *fileListCache {
aliOssFileListCacheOnce.Do(func() {
aliOssFileListCacheInstance = newFileListCache(getFileListCacheTTL(), getFileListCacheMaxEntries())
})
return aliOssFileListCacheInstance
}
func getFileListCacheTTL() time.Duration {
ttlSeconds := config.Config.Record.Cache.FileListTTLSeconds
if ttlSeconds <= 0 {
ttlSeconds = defaultFileListCacheTTLSeconds
}
return time.Duration(ttlSeconds) * time.Second
}
func getFileListCacheMaxEntries() int {
maxEntries := config.Config.Record.Cache.FileListMaxEntries
if maxEntries <= 0 {
maxEntries = defaultFileListCacheMaxEntries
}
return maxEntries
}
type fileListCache struct {
cache *cache.Cache
maxEntries int
mu sync.Mutex
lru *list.List
elements map[string]*list.Element
group singleflight.Group
}
func newFileListCache(ttl time.Duration, maxEntries int) *fileListCache {
if ttl <= 0 {
ttl = defaultFileListCacheTTLSeconds * time.Second
}
if maxEntries <= 0 {
maxEntries = defaultFileListCacheMaxEntries
}
c := cache.New(ttl, fileListCacheCleanupInterval)
result := &fileListCache{
cache: c,
maxEntries: maxEntries,
lru: list.New(),
elements: make(map[string]*list.Element, maxEntries),
}
c.OnEvicted(func(key string, value any) {
result.removeFromLru(key)
})
return result
}
func (c *fileListCache) GetOrLoad(key string, loader func() ([]dto.File, error)) ([]dto.File, bool, bool, error) {
if files, ok := c.Get(key); ok {
return files, true, false, nil
}
value, err, shared := c.group.Do(key, func() (any, error) {
if files, ok := c.Get(key); ok {
return files, nil
}
files, err := loader()
if err != nil {
return nil, err
}
c.Set(key, files)
return files, nil
})
if err != nil {
return nil, false, shared, err
}
files, ok := value.([]dto.File)
if !ok {
return nil, false, shared, fmt.Errorf("缓存返回类型错误: key=%s", key)
}
return files, false, shared, nil
}
func (c *fileListCache) Get(key string) ([]dto.File, bool) {
value, ok := c.cache.Get(key)
if !ok {
c.removeFromLru(key)
return nil, false
}
files, ok := value.([]dto.File)
if !ok {
c.cache.Delete(key)
c.removeFromLru(key)
return nil, false
}
c.touch(key)
return files, true
}
func (c *fileListCache) Set(key string, files []dto.File) {
c.cache.Set(key, files, cache.DefaultExpiration)
c.touch(key)
c.evictIfNeeded()
}
func (c *fileListCache) touch(key string) {
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.elements[key]; ok {
c.lru.MoveToFront(el)
return
}
c.elements[key] = c.lru.PushFront(key)
}
func (c *fileListCache) removeFromLru(key string) {
c.mu.Lock()
defer c.mu.Unlock()
el, ok := c.elements[key]
if !ok {
return
}
c.lru.Remove(el)
delete(c.elements, key)
}
func (c *fileListCache) evictIfNeeded() {
for {
c.mu.Lock()
if len(c.elements) <= c.maxEntries {
c.mu.Unlock()
return
}
el := c.lru.Back()
if el == nil {
c.mu.Unlock()
return
}
key := el.Value.(string)
c.lru.Remove(el)
delete(c.elements, key)
c.mu.Unlock()
// 注意:不要在持有 c.mu 时调用 c.cache.Delete,否则会和 OnEvicted 回调产生锁顺序死锁风险
c.cache.Delete(key)
}
}