chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
This commit is contained in:
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package cache 提供进程内、并发安全的短 TTL 缓存。
|
||||
//
|
||||
// 适用场景:结果聚合统计等"非强一致"数据,用短 TTL 换取数据库实时重算压力的显著降低
|
||||
// (投票结果页、探索页热门投票等)。需注意:进程内存缓存在多实例横向扩容时不会共享,
|
||||
// 因此仅用于可接受短暂陈旧的数据;强一致数据请勿使用本缓存。
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ttlEntry struct {
|
||||
value interface{}
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// TTLCache 并发安全的带 TTL 进程内缓存。
|
||||
type TTLCache struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]ttlEntry
|
||||
maxItems int
|
||||
}
|
||||
|
||||
// NewTTLCache 创建缓存实例。maxItems<=0 时使用默认容量 256;达到容量上限时,
|
||||
// 先淘汰过期项,若仍满则整表清空(避免内存无限增长,短 TTL 下过期项通常已占多数)。
|
||||
func NewTTLCache(maxItems int) *TTLCache {
|
||||
if maxItems <= 0 {
|
||||
maxItems = 256
|
||||
}
|
||||
return &TTLCache{
|
||||
items: make(map[string]ttlEntry, maxItems),
|
||||
maxItems: maxItems,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 读取缓存;命中且未过期返回值,否则返回 (nil, false)。
|
||||
func (c *TTLCache) Get(key string) (interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
e, ok := c.items[key]
|
||||
c.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if time.Now().After(e.expiresAt) {
|
||||
c.mu.Lock()
|
||||
// 双重检查,避免并发重复删除(无影响,仅兜底)
|
||||
if cur, still := c.items[key]; still && time.Now().After(cur.expiresAt) {
|
||||
delete(c.items, key)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
return e.value, true
|
||||
}
|
||||
|
||||
// Set 写入缓存并设定过期时间。
|
||||
func (c *TTLCache) Set(key string, value interface{}, ttl time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.items) >= c.maxItems {
|
||||
// 先淘汰过期项
|
||||
now := time.Now()
|
||||
for k, v := range c.items {
|
||||
if now.After(v.expiresAt) {
|
||||
delete(c.items, k)
|
||||
}
|
||||
}
|
||||
// 仍满(多为短 TTL 下瞬时高频写入):整表重建,保证写入成功且不 OOM
|
||||
if len(c.items) >= c.maxItems {
|
||||
c.items = make(map[string]ttlEntry, c.maxItems)
|
||||
}
|
||||
}
|
||||
c.items[key] = ttlEntry{value: value, expiresAt: time.Now().Add(ttl)}
|
||||
}
|
||||
|
||||
// Delete 删除指定键的缓存项;键不存在时静默返回。
|
||||
// 用于写穿式失效:数据变更(如新增投票提交)后立即清除对应缓存,避免结果页短暂陈旧。
|
||||
func (c *TTLCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key)
|
||||
}
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTTLCacheBasic(t *testing.T) {
|
||||
c := NewTTLCache(8)
|
||||
if _, ok := c.Get("missing"); ok {
|
||||
t.Fatal("expected miss for empty cache")
|
||||
}
|
||||
c.Set("k", "v", time.Minute)
|
||||
v, ok := c.Get("k")
|
||||
if !ok || v.(string) != "v" {
|
||||
t.Fatalf("expected hit with v, got %v %v", v, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTTLCacheExpiry(t *testing.T) {
|
||||
c := NewTTLCache(8)
|
||||
c.Set("k", "v", time.Nanosecond)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
if _, ok := c.Get("k"); ok {
|
||||
t.Fatal("expected expired entry to miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTTLCacheCapacityEviction(t *testing.T) {
|
||||
c := NewTTLCache(4)
|
||||
for i := 0; i < 100; i++ {
|
||||
c.Set(string(rune('a'+i%26))+string(rune('0'+i%10)), i, time.Minute)
|
||||
}
|
||||
// 不 panic 且仍可读
|
||||
if _, ok := c.Get("a0"); ok {
|
||||
// 命中与否均可,关键是内存不失控且无 panic
|
||||
_ = ok
|
||||
}
|
||||
}
|
||||
|
||||
func TestTTLCacheConcurrent(t *testing.T) {
|
||||
c := NewTTLCache(16)
|
||||
var wg sync.WaitGroup
|
||||
for g := 0; g < 20; g++ {
|
||||
wg.Add(1)
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 200; i++ {
|
||||
key := string(rune('a' + (n+i)%26))
|
||||
c.Set(key, n, time.Minute)
|
||||
c.Get(key)
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user