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:
243
internal/cache/l1.go
vendored
243
internal/cache/l1.go
vendored
@@ -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
27
internal/cache/l2.go
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user