246 lines
5.6 KiB
Go
246 lines
5.6 KiB
Go
// 织忆 MemoryWeave — 搜索引擎缓存层(多 Agent 支持)
|
||
// G9: L1 内存 + L2 Redis 两级缓存
|
||
package storage
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// L2CacheBackend 提供可选的 L2 持久化缓存接口
|
||
type L2CacheBackend interface {
|
||
Get(key string) ([]byte, bool)
|
||
Set(key string, data []byte, ttl time.Duration)
|
||
Del(key string)
|
||
}
|
||
|
||
// redisL2Adapter 将同包的 RedisCache 适配为 L2CacheBackend
|
||
type redisL2Adapter struct{}
|
||
|
||
func (a *redisL2Adapter) Get(key string) ([]byte, bool) {
|
||
// 延迟获取 RedisCache 实例(避免循环 init)
|
||
rc := getRedisCacheInstance()
|
||
if rc == nil {
|
||
return nil, false
|
||
}
|
||
return rc.Get(key)
|
||
}
|
||
func (a *redisL2Adapter) Set(key string, data []byte, ttl time.Duration) {
|
||
rc := getRedisCacheInstance()
|
||
if rc == nil {
|
||
return
|
||
}
|
||
rc.Set(key, data)
|
||
}
|
||
func (a *redisL2Adapter) Del(key string) {
|
||
rc := getRedisCacheInstance()
|
||
if rc == nil {
|
||
return
|
||
}
|
||
rc.Del(key)
|
||
}
|
||
|
||
// getRedisCacheInstance 单例获取 RedisCache(延迟初始化)
|
||
var (
|
||
redisCacheInstance *RedisCache
|
||
redisCacheOnce sync.Once
|
||
)
|
||
|
||
func getRedisCacheInstance() *RedisCache {
|
||
redisCacheOnce.Do(func() {
|
||
redisCacheInstance = NewRedisCache(1 * time.Hour)
|
||
})
|
||
return redisCacheInstance
|
||
}
|
||
|
||
// SearchCache 基于 LRU + TTL 的搜索缓存(支持可选 L2 Redis)
|
||
// Get 时:L1 miss → 查 Redis L2 → 回填 L1
|
||
// Set 时:写 L1 + 写 Redis L2(TTL 同步)
|
||
type SearchCache struct {
|
||
mu sync.RWMutex
|
||
entries map[string]*CacheEntry
|
||
maxSize int
|
||
ttl time.Duration
|
||
l2 L2CacheBackend // 可选 L2 缓存(Redis),nil 时只有 L1
|
||
}
|
||
|
||
type CacheEntry struct {
|
||
Key string `json:"key"`
|
||
Results []byte `json:"results"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
Hits int `json:"hits"`
|
||
}
|
||
|
||
// newSearchCache 内部构造器(l2 可为 nil)
|
||
func newSearchCache(maxSize int, ttl time.Duration, l2 L2CacheBackend) *SearchCache {
|
||
sc := &SearchCache{
|
||
entries: make(map[string]*CacheEntry),
|
||
maxSize: maxSize,
|
||
ttl: ttl,
|
||
l2: l2,
|
||
}
|
||
go sc.reaper()
|
||
return sc
|
||
}
|
||
|
||
// NewSearchCache 创建纯 L1 内存 SearchCache(向后兼容)
|
||
func NewSearchCache(maxSize int, ttl time.Duration) *SearchCache {
|
||
return newSearchCache(maxSize, ttl, nil)
|
||
}
|
||
|
||
// NewSearchCacheWithRedis 创建带 Redis L2 的 SearchCache(G9 多级缓存)
|
||
func NewSearchCacheWithRedis(maxSize int, ttl time.Duration) *SearchCache {
|
||
return newSearchCache(maxSize, ttl, &redisL2Adapter{})
|
||
}
|
||
|
||
// Get 获取缓存结果:L1 miss → 查 L2 → 回填 L1
|
||
func (sc *SearchCache) Get(query, namespace string) ([]byte, bool) {
|
||
key := cacheKey(query, namespace)
|
||
|
||
// L1 查找
|
||
sc.mu.RLock()
|
||
entry, ok := sc.entries[key]
|
||
sc.mu.RUnlock()
|
||
|
||
if ok && time.Since(entry.CreatedAt) <= sc.ttl {
|
||
sc.mu.Lock()
|
||
entry.Hits++
|
||
sc.mu.Unlock()
|
||
return entry.Results, true
|
||
}
|
||
|
||
// L1 miss,尝试 L2
|
||
if sc.l2 != nil {
|
||
if data, found := sc.l2.Get(key); found {
|
||
// 回填 L1
|
||
sc.mu.Lock()
|
||
if len(sc.entries) >= sc.maxSize {
|
||
sc.evictLRU()
|
||
}
|
||
sc.entries[key] = &CacheEntry{
|
||
Key: key,
|
||
Results: data,
|
||
CreatedAt: time.Now(),
|
||
Hits: 1,
|
||
}
|
||
sc.mu.Unlock()
|
||
return data, true
|
||
}
|
||
}
|
||
|
||
return nil, false
|
||
}
|
||
|
||
// Set 写入缓存:写 L1 + 写 L2
|
||
func (sc *SearchCache) Set(query, namespace string, results interface{}) {
|
||
key := cacheKey(query, namespace)
|
||
data, err := json.Marshal(results)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
sc.mu.Lock()
|
||
defer sc.mu.Unlock()
|
||
|
||
// L1 驱逐
|
||
if len(sc.entries) >= sc.maxSize {
|
||
sc.evictLRU()
|
||
}
|
||
|
||
sc.entries[key] = &CacheEntry{
|
||
Key: key,
|
||
Results: data,
|
||
CreatedAt: time.Now(),
|
||
Hits: 0,
|
||
}
|
||
|
||
// L2 写入
|
||
if sc.l2 != nil {
|
||
sc.l2.Set(key, data, sc.ttl)
|
||
}
|
||
}
|
||
|
||
// Stats 返回缓存统计
|
||
func (sc *SearchCache) Stats() map[string]interface{} {
|
||
sc.mu.RLock()
|
||
defer sc.mu.RUnlock()
|
||
return map[string]interface{}{
|
||
"size": len(sc.entries),
|
||
"max_size": sc.maxSize,
|
||
"ttl": sc.ttl.String(),
|
||
"l2_enabled": sc.l2 != nil,
|
||
}
|
||
}
|
||
|
||
// Invalidate 使指定 namespace 的缓存失效(L1 + L2)
|
||
func (sc *SearchCache) Invalidate(namespace string) {
|
||
sc.mu.Lock()
|
||
defer sc.mu.Unlock()
|
||
for key, entry := range sc.entries {
|
||
var parsed map[string]interface{}
|
||
json.Unmarshal(entry.Results, &parsed)
|
||
if ns, ok := parsed["namespace"].(string); ok && ns == namespace {
|
||
delete(sc.entries, key)
|
||
if sc.l2 != nil {
|
||
sc.l2.Del(key)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// InvalidateKey 使指定 key 失效(L1 + L2)
|
||
func (sc *SearchCache) InvalidateKey(query, namespace string) {
|
||
key := cacheKey(query, namespace)
|
||
sc.mu.Lock()
|
||
delete(sc.entries, key)
|
||
sc.mu.Unlock()
|
||
if sc.l2 != nil {
|
||
sc.l2.Del(key)
|
||
}
|
||
}
|
||
|
||
func (sc *SearchCache) evictLRU() {
|
||
var oldestKey string
|
||
var oldestTime time.Time
|
||
for key, entry := range sc.entries {
|
||
if oldestKey == "" || entry.CreatedAt.Before(oldestTime) {
|
||
oldestKey = key
|
||
oldestTime = entry.CreatedAt
|
||
}
|
||
}
|
||
if oldestKey != "" {
|
||
delete(sc.entries, oldestKey)
|
||
}
|
||
}
|
||
|
||
func (sc *SearchCache) reaper() {
|
||
ticker := time.NewTicker(5 * time.Minute)
|
||
for range ticker.C {
|
||
sc.mu.Lock()
|
||
now := time.Now()
|
||
for key, entry := range sc.entries {
|
||
if now.Sub(entry.CreatedAt) > sc.ttl {
|
||
delete(sc.entries, key)
|
||
if sc.l2 != nil {
|
||
sc.l2.Del(key)
|
||
}
|
||
}
|
||
}
|
||
sc.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
func cacheKey(query, namespace string) string {
|
||
h := sha256.Sum256([]byte(query + "|" + namespace))
|
||
return fmt.Sprintf("%x", h[:16])
|
||
}
|
||
|
||
// ─── 全局搜索缓存实例(G9:带 Redis L2) ─────────────────────
|
||
var SearchCacheInstance *SearchCache
|
||
|
||
func init() {
|
||
SearchCacheInstance = NewSearchCacheWithRedis(1000, 1*time.Hour)
|
||
} |