轻量 Agent 的会话管理:Redis 与本地缓存的混合存储策略与过期设计
2026/7/10 22:51:17 网站建设 项目流程

轻量 Agent 的会话管理:Redis 与本地缓存的混合存储策略与过期设计

一、Agent 会话的存储悖论:既需要毫秒级读取,又需要持久化

Agent 的每一次对话都有一个上下文——之前的对话历史、调用过的工具、中间的计算状态。这些数据如果每次都从数据库加载,IO 开销会显著增加响应延迟。如果完全放在内存中,服务重启后所有会话丢失,用户会发现自己和 Agent 的对话"被重置了"。

这就是会话存储的经典两难:内存快但不持久,数据库持久但不够快。

Redis 似乎是最佳选择——它既有内存的速度,又有 AOF/RDB 的持久化。但对于独立开发者或小团队维护的 Agent 项目,单独部署 Redis 集群是一种奢侈。混合存储策略提供了一种折中方案:热数据在本地内存(LRU Cache),全量数据持久化到 SQLite 或文件

graph TB A[读取会话] --> B{本地 LRU Cache 命中?} B -->|是| C[直接返回<br/>< 0.1ms] B -->|否| D{Redis 可用且命中?} D -->|是| E[返回 + 写回本地 Cache<br/>1-5ms] D -->|否| F[从持久化层读取<br/>SQLite/文件] F --> G[返回 + 写回 Cache<br/>5-50ms] H[写入会话] --> I[更新本地 Cache] I --> J{Redis 可用?} J -->|是| K[异步写入 Redis] J -->|否| L[标记待同步] I --> M[异步持久化] style C fill:#51cf66,color:#fff style G fill:#ffd43b,color:#000

二、混合存储的三层架构:本地 → Redis → 持久化

Layer 1 — 本地 LRU Cache(最快但容量小)

使用固定大小的 LRU(最近最少使用)缓存存储最热的会话。当 Agent 正在处理一个会话时,这个会话的数据会被频繁读写,本地缓存避免了网络 IO。

关键在于缓存大小的设定。如果 Agent 的并发会话数峰值是 50,本地缓存设为 100 就足够。超出部分自动被 LRU 淘汰。

Layer 2 — Redis(快且支持过期,但需要额外运维)

Redis 的 TTL(过期时间)非常适合会话管理。每个会话可以设置一个过期时间(如 24 小时),超时自动清理。Redis 还提供原子操作(如追加对话历史),不用加锁。

但如果 Redis 不可用(网络故障、资源不足),系统不能崩溃。降级策略是:跳过 Redis 直接读写持久化层。

Layer 3 — 持久化存储(最慢但可靠性最高)

SQLite 是一个出乎意料的好选择。它是单文件数据库,不需要独立的服务进程,读性能对于几百条记录来说是亚毫秒级别的。对于独立开发者,SQLite 比 PostgreSQL 更适合——零运维。

三、混合存储的完整实现

package session import ( "context" "encoding/json" "fmt" "sync" "time" lru "github.com/hashicorp/golang-lru/v2" ) // Session 数据结构 type Session struct { ID string `json:"id"` UserID string `json:"user_id"` Messages []Message `json:"messages"` State string `json:"state"` // Agent 当前状态 Metadata map[string]interface{} `json:"metadata"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` TTL int64 `json:"ttl"` // 过期时间戳 } type Message struct { Role string `json:"role"` Content string `json:"content"` Time int64 `json:"time"` } // Store 混合存储管理器 type Store struct { local *lru.Cache[string, *Session] // 本地 LRU 缓存 redis *RedisClient // Redis(可选,可为 nil) db *SQLiteStore // SQLite 持久化 mu sync.RWMutex closed bool } // 存储接口,方便替换后端 type Backend interface { Get(ctx context.Context, id string) (*Session, error) Set(ctx context.Context, session *Session) error Delete(ctx context.Context, id string) error ListByUser(ctx context.Context, userID string) ([]*Session, error) } func NewStore(redisURL string, dbPath string, localSize int) (*Store, error) { // 初始化本地缓存 cache, err := lru.New[string, *Session](localSize) if err != nil { return nil, fmt.Errorf("create lru cache: %w", err) } // 初始化 SQLite sqlite, err := NewSQLiteStore(dbPath) if err != nil { return nil, fmt.Errorf("create sqlite store: %w", err) } // Redis 是可选的 var redis *RedisClient if redisURL != "" { redis, err = NewRedisClient(redisURL) if err != nil { // Redis 失败不应该阻止启动,记录警告即可 fmt.Printf("WARNING: Redis unavailable: %v\n", err) redis = nil } } return &Store{ local: cache, redis: redis, db: sqlite, }, nil } // Get 读取会话(按优先级:本地 → Redis → SQLite) func (s *Store) Get(ctx context.Context, id string) (*Session, error) { // Layer 1: 本地缓存 if sess, ok := s.local.Get(id); ok { if !sess.IsExpired() { return sess, nil } // 过期了,从本地缓存中移除 s.local.Remove(id) } // Layer 2: Redis if s.redis != nil { sess, err := s.redis.Get(ctx, id) if err == nil && sess != nil { if !sess.IsExpired() { s.local.Add(id, sess) // 回写到本地缓存 return sess, nil } s.redis.Delete(ctx, id) } } // Layer 3: SQLite sess, err := s.db.Get(ctx, id) if err != nil { return nil, fmt.Errorf("get session from db: %w", err) } if sess == nil { return nil, fmt.Errorf("session %s not found", id) } if sess.IsExpired() { s.db.Delete(ctx, id) return nil, fmt.Errorf("session %s expired", id) } // 回写缓存 s.local.Add(id, sess) if s.redis != nil { go s.redis.Set(context.Background(), sess) // 异步写 Redis } return sess, nil } // Set 保存会话(同步写本地 + 异步写 Redis/SQLite) func (s *Store) Set(ctx context.Context, session *Session) error { session.UpdatedAt = time.Now() // Layer 1: 立即更新本地缓存 s.local.Add(session.ID, session) // Layer 2 & 3: 异步写入 go func() { if s.redis != nil { if err := s.redis.Set(context.Background(), session); err != nil { fmt.Printf("WARNING: redis set failed: %v\n", err) } } if err := s.db.Set(context.Background(), session); err != nil { fmt.Printf("ERROR: db set failed: %v\n", err) } }() return nil } // Delete 删除会话 func (s *Store) Delete(ctx context.Context, id string) error { s.local.Remove(id) var errs []error if s.redis != nil { if err := s.redis.Delete(ctx, id); err != nil { errs = append(errs, fmt.Errorf("redis: %w", err)) } } if err := s.db.Delete(ctx, id); err != nil { errs = append(errs, fmt.Errorf("db: %w", err)) } if len(errs) > 0 { return fmt.Errorf("delete failures: %v", errs) } return nil } // IsExpired 检查会话是否过期 func (s *Session) IsExpired() bool { if s.TTL == 0 { return false // 0 表示永不过期 } return time.Now().Unix() > s.TTL } // Cleanup 后台定期清理过期会话 func (s *Store) StartCleanup(interval time.Duration) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { s.cleanupExpired() } }() } func (s *Store) cleanupExpired() { // 本地缓存中的过期会话由 LRU 自动处理 // 这里清理持久化存储中的过期会话 expired, _ := s.db.FindExpired() for _, id := range expired { s.Delete(context.Background(), id) } } func (s *Store) Close() error { s.mu.Lock() defer s.mu.Unlock() s.closed = true // 等待异步写入完成 time.Sleep(100 * time.Millisecond) return s.db.Close() }

四、混合存储的权衡与陷阱

数据一致性:异步写入可能导致"本地缓存已更新但数据库尚未写入"的窗口。如果此时服务崩溃,最近一次会话更新可能丢失。对于聊天应用,这个风险可以接受。对于金融或医疗等严格一致性要求的场景,应改为同步写入。

内存占用:本地 LRU 缓存的大小需要根据实际的并发会话数和每条会话的平均大小来设定。一条会话可能有 50 条消息,约 10KB。缓存 100 条会话约 1MB——影响微乎其微。

Redis 故障降级:当 Redis 不可用时,Store 自动跳过 Redis 层,直接读写 SQLite。这时响应延迟会从毫秒级升到 10-50 毫秒,但服务不会崩溃。

不适用场景

  • 需要跨实例共享会话的分布式部署(本地缓存不可共享)
  • 会话量极大(>10 万),SQLite 的写入性能成为瓶颈
  • 实时协作场景(多个 Agent 同时修改同一个会话)

五、总结

混合存储策略用最小的运维成本实现了"快速读取 + 可靠持久化"的平衡。本地缓存处理热点会话的读写,Redis/SQLite 提供持久化和更大容量的存储。

落地路径:先从最简单的 SQLite + 本地 LRU Cache 开始(零运维依赖);当并发量增长到 SQLite 成为瓶颈时,引入 Redis 作为中间缓存层;当需要多实例部署时,考虑将 SQLite 替换为 PostgreSQL。

少即是多。对于独立开发者,SQLite + 本地 LRU Cache 已经是一个"够用 90% 场景"的会话存储方案。在真正需要 Redis 之前,不要引入 Redis。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询