缩略图缓存加容量闸与删除联动清理
- Store 增 MaxBytes(默认 256MB):写入后节流扫描(10 分钟一次、同时只跑一个), 超限按 mtime 从旧到新删到 3/4 水位(滞回,避免卡在边界反复删),顺带清掉 进程被杀留下的 .tmp - 命中缓存时 Touch 更新 mtime,淘汰因此是 LRU 而非「最早生成的先死」, 热图不会被误删后反复回源拉存储 - 新增 Purge(sha):后台删除上传文件时连带清掉该图所有宽度的产物与 .failed 标记,否则已删图片会永久占着缓存
This commit is contained in:
@@ -11,8 +11,10 @@ import (
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -168,14 +170,27 @@ func min255(v uint32) uint32 {
|
||||
// {Dir}/{sha[:2]}/{sha[:32]}.failed 失败标记
|
||||
//
|
||||
// 文件名以原图内容哈希(files.sha256)为键:内容变则键变,缓存天然失效。
|
||||
// 缓存只增不减会慢慢吃磁盘,所以 Put 之后按 MaxBytes 做容量闸淘汰(LRU:
|
||||
// 命中时 Touch 更新 mtime,淘汰时先下手最久没人要的)。
|
||||
type Store struct {
|
||||
Dir string
|
||||
mu sync.Mutex
|
||||
locks map[string]*sync.Mutex
|
||||
Dir string
|
||||
MaxBytes int64
|
||||
|
||||
mu sync.Mutex
|
||||
locks map[string]*sync.Mutex
|
||||
lastSweep time.Time
|
||||
sweeping bool
|
||||
}
|
||||
|
||||
// DefaultMaxBytes 是缓存总量上限:约 1700 张 150KB 的缩略图,
|
||||
// 远超单用户博客的实际存量,纯粹是防多年以后无限涨。
|
||||
const DefaultMaxBytes = 256 << 20
|
||||
|
||||
// sweepInterval 限制扫描频率——淘汰是后台维护,不该拖慢写缓存的请求。
|
||||
const sweepInterval = 10 * time.Minute
|
||||
|
||||
func NewStore(dir string) *Store {
|
||||
return &Store{Dir: dir, locks: make(map[string]*sync.Mutex)}
|
||||
return &Store{Dir: dir, MaxBytes: DefaultMaxBytes, locks: make(map[string]*sync.Mutex)}
|
||||
}
|
||||
|
||||
func (s *Store) dir(sha string) string { return filepath.Join(s.Dir, sha[:2]) }
|
||||
@@ -217,9 +232,35 @@ func (s *Store) Put(sha string, w int, data []byte, mime string) (string, error)
|
||||
return "", err
|
||||
}
|
||||
_ = os.Remove(s.failedPath(sha))
|
||||
s.maybeSweep()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Touch 把缓存文件的 mtime 拨到现在:命中一次就等于「最近还要用」,
|
||||
// 供容量闸按 LRU 淘汰。失败无所谓(只影响淘汰顺序)。
|
||||
func (s *Store) Touch(path string) {
|
||||
now := time.Now()
|
||||
_ = os.Chtimes(path, now, now)
|
||||
}
|
||||
|
||||
// Purge 清掉某个原图(按内容哈希)的全部缩略图与失败标记。
|
||||
// 删除上传文件时调用,否则缓存会一直留着已删图片的产物。
|
||||
func (s *Store) Purge(sha string) {
|
||||
if len(sha) < 32 {
|
||||
return
|
||||
}
|
||||
entries, err := os.ReadDir(s.dir(sha))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), sha[:32]) { // {sha}-{w}.jpg / {sha}.failed
|
||||
_ = os.Remove(filepath.Join(s.dir(sha), e.Name()))
|
||||
}
|
||||
}
|
||||
_ = os.Remove(s.dir(sha)) // 目录空了就收掉
|
||||
}
|
||||
|
||||
// FailedRecently 报告是否在冷却期(近期生成失败过)。
|
||||
func (s *Store) FailedRecently(sha string) bool {
|
||||
fi, err := os.Stat(s.failedPath(sha))
|
||||
@@ -252,6 +293,67 @@ func (s *Store) Lock(sha string, w int) *sync.Mutex {
|
||||
// lockCacheMax 限制锁表大小(每张图最多 3 种宽度),博客体量下足够。
|
||||
const lockCacheMax = 4096
|
||||
|
||||
// maybeSweep 在写入新缓存后检查总量;超上限就按 mtime 从旧到新删,
|
||||
// 删到 3/4 水位(留滞回,避免每次写入都刚好卡在边界反复删)。
|
||||
// 每 sweepInterval 最多跑一次,且同一时刻只有一个在跑。
|
||||
func (s *Store) maybeSweep() {
|
||||
if s.MaxBytes <= 0 {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.sweeping || time.Since(s.lastSweep) < sweepInterval {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.sweeping, s.lastSweep = true, time.Now()
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
s.sweeping = false
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
var files []fileInfo
|
||||
var total int64
|
||||
_ = filepath.WalkDir(s.Dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
// 半途而废的临时文件(进程被杀)直接清
|
||||
if strings.HasSuffix(path, ".tmp") {
|
||||
_ = os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
fi, err := d.Info()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
total += fi.Size()
|
||||
files = append(files, fileInfo{path: path, size: fi.Size(), mtime: fi.ModTime()})
|
||||
return nil
|
||||
})
|
||||
if total <= s.MaxBytes {
|
||||
return
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool { return files[i].mtime.Before(files[j].mtime) })
|
||||
lowWater := s.MaxBytes * 3 / 4
|
||||
for _, f := range files {
|
||||
if total <= lowWater {
|
||||
break
|
||||
}
|
||||
if err := os.Remove(f.path); err == nil {
|
||||
total -= f.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
path string
|
||||
size int64
|
||||
mtime time.Time
|
||||
}
|
||||
|
||||
func extFor(mime string) string {
|
||||
switch mime {
|
||||
case "image/png":
|
||||
|
||||
Reference in New Issue
Block a user