- Add nolint comments for sync.Pool.Get() type assertions (pool always returns valid pointers) - Extract TLS version strings to constants in sslutil/tlsconfig.go - Extract expires directive strings to constants in handler/static.go Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
266 lines
6.2 KiB
Go
266 lines
6.2 KiB
Go
// Package cache 提供分片缓存实现,用于高并发场景。
|
||
//
|
||
// 该文件实现分片文件缓存,将缓存分散到多个独立分片,
|
||
// 每个分片有自己的锁和 LRU 链表,减少锁竞争。
|
||
//
|
||
// 主要用途:
|
||
//
|
||
// 用于高并发场景下的文件缓存,减少单一 RWMutex 的竞争压力。
|
||
//
|
||
// 注意事项:
|
||
// - 分片数固定为 16,按 path hash 选择分片
|
||
// - 各分片独立 LRU 淘汰,无法跨分片协调
|
||
// - 适合读多写少场景,写入仍会阻塞单个分片
|
||
//
|
||
// 作者:xfy
|
||
package cache
|
||
|
||
import (
|
||
"container/list"
|
||
"hash/fnv"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
const shardCount = 16
|
||
|
||
// FileCacheShard 单个缓存分片。
|
||
type FileCacheShard struct {
|
||
entries map[string]*FileEntry
|
||
lruList *list.List
|
||
maxEntries int64
|
||
maxSize int64
|
||
inactive time.Duration
|
||
currentSize int64
|
||
mu sync.RWMutex
|
||
entryPool sync.Pool
|
||
}
|
||
|
||
// ShardedFileCache 分片文件缓存。
|
||
type ShardedFileCache struct {
|
||
shards [shardCount]*FileCacheShard
|
||
maxEntries int64
|
||
maxSize int64
|
||
inactive time.Duration
|
||
}
|
||
|
||
// NewShardedFileCache 创建分片文件缓存实例。
|
||
func NewShardedFileCache(maxEntries, maxSize int64, inactive time.Duration) *ShardedFileCache {
|
||
s := &ShardedFileCache{
|
||
maxEntries: maxEntries,
|
||
maxSize: maxSize,
|
||
inactive: inactive,
|
||
}
|
||
|
||
perShardEntries := maxEntries / shardCount
|
||
if perShardEntries == 0 {
|
||
perShardEntries = maxEntries // 单分片容量
|
||
}
|
||
perShardSize := maxSize / shardCount
|
||
if perShardSize == 0 {
|
||
perShardSize = maxSize
|
||
}
|
||
|
||
for i := range shardCount {
|
||
shard := &FileCacheShard{
|
||
maxEntries: perShardEntries,
|
||
maxSize: perShardSize,
|
||
inactive: inactive,
|
||
entries: make(map[string]*FileEntry),
|
||
lruList: list.New(),
|
||
}
|
||
shard.entryPool = sync.Pool{
|
||
New: func() any {
|
||
return &FileEntry{}
|
||
},
|
||
}
|
||
s.shards[i] = shard
|
||
}
|
||
return s
|
||
}
|
||
|
||
// getShard 根据 path hash 选择分片。
|
||
func (s *ShardedFileCache) getShard(path string) *FileCacheShard {
|
||
h := fnv.New64a()
|
||
h.Write([]byte(path))
|
||
return s.shards[h.Sum64()%shardCount]
|
||
}
|
||
|
||
// Get 获取缓存的文件。
|
||
func (s *ShardedFileCache) Get(path string) (*FileEntry, bool) {
|
||
return s.getShard(path).Get(path)
|
||
}
|
||
|
||
// Set 设置缓存条目。
|
||
func (s *ShardedFileCache) Set(path string, data []byte, size int64, modTime time.Time) error {
|
||
return s.getShard(path).Set(path, data, size, modTime)
|
||
}
|
||
|
||
// Delete 删除缓存条目。
|
||
func (s *ShardedFileCache) Delete(path string) {
|
||
s.getShard(path).Delete(path)
|
||
}
|
||
|
||
// Clear 清空所有分片缓存。
|
||
func (s *ShardedFileCache) Clear() {
|
||
for _, shard := range s.shards {
|
||
shard.Clear()
|
||
}
|
||
}
|
||
|
||
// Stats 返回汇总统计信息。
|
||
func (s *ShardedFileCache) Stats() FileCacheStats {
|
||
totalEntries := int64(0)
|
||
totalSize := int64(0)
|
||
for _, shard := range s.shards {
|
||
stats := shard.Stats()
|
||
totalEntries += stats.Entries
|
||
totalSize += stats.Size
|
||
}
|
||
return FileCacheStats{
|
||
Entries: totalEntries,
|
||
MaxEntries: s.maxEntries,
|
||
Size: totalSize,
|
||
MaxSize: s.maxSize,
|
||
}
|
||
}
|
||
|
||
// --- FileCacheShard 方法(内联实现,避免依赖 FileCache)---
|
||
|
||
// Get 获取缓存的文件。
|
||
func (sh *FileCacheShard) Get(path string) (*FileEntry, bool) {
|
||
sh.mu.RLock()
|
||
entry, ok := sh.entries[path]
|
||
if !ok {
|
||
sh.mu.RUnlock()
|
||
return nil, false
|
||
}
|
||
|
||
// 检查是否过期
|
||
if time.Since(entry.LastAccess) > sh.inactive {
|
||
sh.mu.RUnlock()
|
||
sh.mu.Lock()
|
||
if entry, ok = sh.entries[path]; ok && time.Since(entry.LastAccess) > sh.inactive {
|
||
sh.removeEntry(entry)
|
||
}
|
||
sh.mu.Unlock()
|
||
return nil, false
|
||
}
|
||
|
||
sh.mu.RUnlock()
|
||
|
||
// 更新访问时间(需写锁)
|
||
sh.mu.Lock()
|
||
if entry, ok = sh.entries[path]; ok && time.Since(entry.LastAccess) <= sh.inactive {
|
||
entry.LastAccess = time.Now()
|
||
sh.lruList.MoveToFront(entry.element)
|
||
}
|
||
sh.mu.Unlock()
|
||
return entry, true
|
||
}
|
||
|
||
// Set 设置缓存条目。
|
||
func (sh *FileCacheShard) Set(path string, data []byte, size int64, modTime time.Time) error {
|
||
sh.mu.Lock()
|
||
defer sh.mu.Unlock()
|
||
|
||
if entry, ok := sh.entries[path]; ok {
|
||
sh.currentSize -= entry.Size
|
||
entry.Data = data
|
||
entry.Size = size
|
||
entry.ModTime = modTime
|
||
entry.CachedAt = time.Now()
|
||
entry.LastAccess = time.Now()
|
||
sh.currentSize += size
|
||
sh.lruList.MoveToFront(entry.element)
|
||
sh.evictIfNeeded()
|
||
return nil
|
||
}
|
||
|
||
entry := sh.entryPool.Get().(*FileEntry) //nolint:errcheck // pool always returns valid *FileEntry
|
||
entry.Path = path
|
||
entry.Data = data
|
||
entry.Size = size
|
||
entry.ModTime = modTime
|
||
entry.CachedAt = time.Now()
|
||
entry.LastAccess = time.Now()
|
||
entry.element = sh.lruList.PushFront(entry)
|
||
sh.entries[path] = entry
|
||
sh.currentSize += size
|
||
|
||
sh.evictIfNeeded()
|
||
return nil
|
||
}
|
||
|
||
// Delete 删除缓存条目。
|
||
func (sh *FileCacheShard) Delete(path string) {
|
||
sh.mu.Lock()
|
||
defer sh.mu.Unlock()
|
||
if entry, ok := sh.entries[path]; ok {
|
||
sh.removeEntry(entry)
|
||
}
|
||
}
|
||
|
||
// Clear 清空分片缓存。
|
||
func (sh *FileCacheShard) Clear() {
|
||
sh.mu.Lock()
|
||
defer sh.mu.Unlock()
|
||
sh.entries = make(map[string]*FileEntry)
|
||
sh.lruList = list.New()
|
||
sh.currentSize = 0
|
||
}
|
||
|
||
// Stats 返回分片统计信息。
|
||
func (sh *FileCacheShard) Stats() FileCacheStats {
|
||
sh.mu.RLock()
|
||
defer sh.mu.RUnlock()
|
||
return FileCacheStats{
|
||
Entries: int64(len(sh.entries)),
|
||
MaxEntries: sh.maxEntries,
|
||
Size: sh.currentSize,
|
||
MaxSize: sh.maxSize,
|
||
}
|
||
}
|
||
|
||
// removeEntry 内部删除条目(不加锁)。
|
||
func (sh *FileCacheShard) removeEntry(entry *FileEntry) {
|
||
sh.lruList.Remove(entry.element)
|
||
delete(sh.entries, entry.Path)
|
||
sh.currentSize -= entry.Size
|
||
// Reset entry 并放回池
|
||
entry.Path = ""
|
||
entry.Data = nil
|
||
entry.Size = 0
|
||
entry.ModTime = time.Time{}
|
||
entry.CachedAt = time.Time{}
|
||
entry.LastAccess = time.Time{}
|
||
entry.element = nil
|
||
sh.entryPool.Put(entry)
|
||
}
|
||
|
||
// evictIfNeeded 根据限制淘汰条目。
|
||
func (sh *FileCacheShard) evictIfNeeded() {
|
||
for sh.lruList.Len() > int(sh.maxEntries) && sh.maxEntries > 0 {
|
||
sh.evictLRU()
|
||
}
|
||
for sh.currentSize > sh.maxSize && sh.maxSize > 0 {
|
||
sh.evictLRU()
|
||
}
|
||
}
|
||
|
||
// evictLRU 淘汰最久未使用的条目。
|
||
func (sh *FileCacheShard) evictLRU() {
|
||
if sh.lruList.Len() == 0 {
|
||
return
|
||
}
|
||
element := sh.lruList.Back()
|
||
if element == nil {
|
||
return
|
||
}
|
||
entry, ok := element.Value.(*FileEntry)
|
||
if !ok {
|
||
return
|
||
}
|
||
sh.removeEntry(entry)
|
||
}
|