memoryweave/go/internal/storage/searchcache.go

246 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 织忆 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 L2TTL 同步)
type SearchCache struct {
mu sync.RWMutex
entries map[string]*CacheEntry
maxSize int
ttl time.Duration
l2 L2CacheBackend // 可选 L2 缓存Redisnil 时只有 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 的 SearchCacheG9 多级缓存)
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)
}