perf: Sprint 19 P0/P1 性能优化落地

P0(高优先级):
- P0-1: 确认数据库复合索引已存在(GORM tag),composite_index_test 验证通过
- P0-2: 连接池调优 MaxIdleConns 5→10, ConnMaxLifetime 30min→5min
- P0-3: Redis 智能探测(ProbeRedis),无 Redis 自动降级到纯内存模式

P1(中优先级):
- P1-1: GZIP 压缩中间件(compress/gzip 标准库,零新依赖)
- P1-2: 权限缓存 TTL 30min→5min
- P1-3: Argon2id 启动自适应校准(CalibrateArgon2id)

历史优化(含本次提交):
- L1Cache O(n)→O(1) LRU 重构
- Auth 中间件 DB 查询合并 + 5s L1 缓存
- Logger 异步化(4096 缓冲通道)

验证: go build/vet/test 41/41 PASS, govulncheck 无漏洞
This commit is contained in:
2026-04-18 22:57:44 +08:00
parent 85285c16d1
commit 7b047e2f11
11 changed files with 1231 additions and 154 deletions

243
internal/cache/l1.go vendored
View File

@@ -1,212 +1,237 @@
// Package cache provides in-memory L1 cache with true O(1) LRU eviction.
//
// Implementation uses a doubly-linked list + hash-map, giving O(1) for Get, Set,
// Delete and eviction — compared to the previous O(n) slice-scan approach which
// became a bottleneck under high concurrency (10 000-item cache, 1 000+ QPS).
//
// Thread-safety: a single sync.RWMutex guards the whole structure.
// Reads (Get) promote the entry to MRU and therefore must take a write lock.
// If read-heavy workloads dominate, consider a sharded variant.
package cache
import (
"container/list"
"sync"
"time"
)
const (
// maxItems 是L1Cache的最大条目数
// 超过此限制后将淘汰最久未使用的条目
maxItems = 10000
// defaultMaxItems is the maximum number of entries held in L1Cache.
// Entries beyond this limit are evicted using LRU policy (O(1)).
defaultMaxItems = 10000
)
// CacheItem 缓存项
// CacheItem holds a cached value together with its expiry timestamp.
type CacheItem struct {
Value interface{}
Expiration int64
Expiration int64 // UnixNano; 0 means no expiration
}
// Expired 判断缓存项是否过期
// Expired reports whether this item has passed its TTL.
func (item *CacheItem) Expired() bool {
return item.Expiration > 0 && time.Now().UnixNano() > item.Expiration
}
// L1Cache L1本地缓存支持LRU淘汰策略
type L1Cache struct {
items map[string]*CacheItem
mu sync.RWMutex
// accessOrder 记录key的访问顺序用于LRU淘汰
// 第一个是最久未使用的,最后一个是最近使用的
accessOrder []string
// lruEntry is the value stored inside the doubly-linked list element.
type lruEntry struct {
key string
item *CacheItem
}
// NewL1Cache 创建L1缓存
// L1Cache is an in-process LRU cache backed by a hash-map and a doubly-linked
// list. All exported methods are safe for concurrent use.
type L1Cache struct {
mu sync.RWMutex
items map[string]*list.Element // key → list element
lruList *list.List // front = MRU, back = LRU
maxItems int
}
// NewL1Cache creates a new L1Cache with the default capacity (10 000 items).
func NewL1Cache() *L1Cache {
return &L1Cache{
items: make(map[string]*CacheItem),
items: make(map[string]*list.Element, defaultMaxItems),
lruList: list.New(),
maxItems: defaultMaxItems,
}
}
// Set 设置缓存
func (c *L1Cache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
// NewL1CacheWithSize creates a new L1Cache with a custom capacity.
func NewL1CacheWithSize(maxItems int) *L1Cache {
if maxItems <= 0 {
maxItems = defaultMaxItems
}
return &L1Cache{
items: make(map[string]*list.Element, maxItems),
lruList: list.New(),
maxItems: maxItems,
}
}
// Set inserts or updates key with the given value and TTL.
// A zero or negative TTL means the entry never expires.
// O(1) amortised.
func (c *L1Cache) Set(key string, value interface{}, ttl time.Duration) {
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
// 如果key已存在更新访问顺序
if _, exists := c.items[key]; exists {
c.items[key] = &CacheItem{
Value: value,
Expiration: expiration,
}
c.updateAccessOrder(key)
c.mu.Lock()
defer c.mu.Unlock()
if elem, ok := c.items[key]; ok {
// Update existing entry and move to front (MRU).
c.lruList.MoveToFront(elem)
entry := elem.Value.(*lruEntry)
entry.item = &CacheItem{Value: value, Expiration: expiration}
return
}
// 检查是否超过最大容量进行LRU淘汰
if len(c.items) >= maxItems {
// Evict LRU entry if at capacity.
if c.lruList.Len() >= c.maxItems {
c.evictLRU()
}
c.items[key] = &CacheItem{
Value: value,
Expiration: expiration,
// Insert new entry at front.
entry := &lruEntry{
key: key,
item: &CacheItem{Value: value, Expiration: expiration},
}
c.accessOrder = append(c.accessOrder, key)
elem := c.lruList.PushFront(entry)
c.items[key] = elem
}
// evictLRU 淘汰最久未使用的条目
// evictLRU removes the least-recently-used entry. Must be called with c.mu held.
func (c *L1Cache) evictLRU() {
if len(c.accessOrder) == 0 {
back := c.lruList.Back()
if back == nil {
return
}
// 淘汰最久未使用的(第一个)
oldest := c.accessOrder[0]
delete(c.items, oldest)
c.accessOrder = c.accessOrder[1:]
entry := back.Value.(*lruEntry)
delete(c.items, entry.key)
c.lruList.Remove(back)
}
// removeFromAccessOrder 从访问顺序中移除key
func (c *L1Cache) removeFromAccessOrder(key string) {
for i, k := range c.accessOrder {
if k == key {
c.accessOrder = append(c.accessOrder[:i], c.accessOrder[i+1:]...)
return
}
}
}
// updateAccessOrder 更新访问顺序将key移到最后最近使用
func (c *L1Cache) updateAccessOrder(key string) {
for i, k := range c.accessOrder {
if k == key {
// 移除当前位置
c.accessOrder = append(c.accessOrder[:i], c.accessOrder[i+1:]...)
// 添加到末尾
c.accessOrder = append(c.accessOrder, key)
return
}
}
}
// Get 获取缓存
// Get retrieves a value from the cache.
// On a hit the entry is promoted to MRU (requires write lock).
// On expiry the entry is removed and (nil, false) is returned.
// O(1).
func (c *L1Cache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
item, ok := c.items[key]
elem, ok := c.items[key]
if !ok {
return nil, false
}
if item.Expired() {
entry := elem.Value.(*lruEntry)
if entry.item.Expired() {
c.lruList.Remove(elem)
delete(c.items, key)
c.removeFromAccessOrder(key)
return nil, false
}
// 更新访问顺序
c.updateAccessOrder(key)
return item.Value, true
// Promote to MRU.
c.lruList.MoveToFront(elem)
return entry.item.Value, true
}
// Delete 删除缓存
// Delete removes a key from the cache. No-op if the key is absent.
// O(1).
func (c *L1Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
c.removeFromAccessOrder(key)
if elem, ok := c.items[key]; ok {
c.lruList.Remove(elem)
delete(c.items, key)
}
}
// Clear 清空缓存
// Clear removes all entries from the cache.
func (c *L1Cache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = make(map[string]*CacheItem)
c.accessOrder = make([]string, 0)
c.items = make(map[string]*list.Element, c.maxItems)
c.lruList.Init()
}
// Size 获取缓存大小
// Size returns the number of entries currently held (including potentially
// expired ones that have not yet been evicted).
func (c *L1Cache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.items)
}
// Cleanup 清理过期缓存
// Cleanup scans all entries and removes those that have expired.
// This is a background maintenance operation; normal eviction is lazy (on Get).
func (c *L1Cache) Cleanup() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now().UnixNano()
keysToDelete := make([]string, 0)
for key, item := range c.items {
if item.Expiration > 0 && now > item.Expiration {
keysToDelete = append(keysToDelete, key)
var toRemove []*list.Element
for elem := c.lruList.Back(); elem != nil; elem = elem.Prev() {
entry := elem.Value.(*lruEntry)
if entry.item.Expiration > 0 && now > entry.item.Expiration {
toRemove = append(toRemove, elem)
}
}
for _, key := range keysToDelete {
delete(c.items, key)
c.removeFromAccessOrder(key)
for _, elem := range toRemove {
entry := elem.Value.(*lruEntry)
delete(c.items, entry.key)
c.lruList.Remove(elem)
}
}
// Increment 原子递增(用于登录失败计数器等原子操作场景)
// Increment atomically adds delta to the int64 counter stored at key,
// creating it with value delta if it does not exist.
// Used for rate-limit counters, login-failure counters, etc.
// O(1).
func (c *L1Cache) Increment(key string, delta int64, ttl time.Duration) int64 {
c.mu.Lock()
defer c.mu.Unlock()
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
current := int64(0)
if item, ok := c.items[key]; ok {
if item.Expired() {
delete(c.items, key)
c.removeFromAccessOrder(key)
} else {
if v, ok := item.Value.(int64); ok {
c.mu.Lock()
defer c.mu.Unlock()
if elem, ok := c.items[key]; ok {
entry := elem.Value.(*lruEntry)
if !entry.item.Expired() {
current := int64(0)
switch v := entry.item.Value.(type) {
case int64:
current = v
} else if v, ok := item.Value.(int); ok {
case int:
current = int64(v)
} else if v, ok := item.Value.(float64); ok {
case float64:
current = int64(v)
}
newVal := current + delta
entry.item = &CacheItem{Value: newVal, Expiration: expiration}
c.lruList.MoveToFront(elem)
return newVal
}
// Expired: remove and recreate below.
c.lruList.Remove(elem)
delete(c.items, key)
}
newVal := current + delta
c.items[key] = &CacheItem{
Value: newVal,
Expiration: expiration,
// Key absent or expired: insert fresh counter.
if c.lruList.Len() >= c.maxItems {
c.evictLRU()
}
if _, exists := c.items[key]; !exists {
c.accessOrder = append(c.accessOrder, key)
} else {
c.updateAccessOrder(key)
entry := &lruEntry{
key: key,
item: &CacheItem{Value: delta, Expiration: expiration},
}
return newVal
elem := c.lruList.PushFront(entry)
c.items[key] = elem
return delta
}

27
internal/cache/l2.go vendored
View File

@@ -4,12 +4,39 @@ import (
"context"
"encoding/json"
"errors"
"log"
"strings"
"time"
redis "github.com/redis/go-redis/v9"
)
// ProbeRedis 探测 Redis 是否可达。
//
// 使用 2 秒超时发起 PING成功返回 true任何错误连接拒绝、超时、DNS 解析失败)
// 均返回 false 并打印 warn 日志,调用方可据此决定是否启用 Redis。
//
// 此函数是幂等的,可在启动阶段安全调用多次。
func ProbeRedis(addr, password string, db int) bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
client := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
DialTimeout: 2 * time.Second,
})
defer client.Close()
if err := client.Ping(ctx).Err(); err != nil {
log.Printf("redis probe: unreachable at %s — falling back to in-memory only (%v)", addr, err)
return false
}
log.Printf("redis probe: reachable at %s — Redis L2 cache will be enabled", addr)
return true
}
// L2Cache defines the distributed cache contract.
type L2Cache interface {
Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error