ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent 高并发实战:Redis 会话缓存、工具缓存与分布式锁

AI Agent 高并发实战:Redis 会话缓存、工具缓存与分布式锁 1. 从一次线上抖动说起AI Agent 为什么绕不开 Redis做 AI Agent 项目的人迟早会撞上一个很尴尬的场景本地跑得好好的智能体一上生产环境就开始抽风。用户发一句“帮我查一下上周的订单”Agent 要拆解意图、调用工具、检索记忆、拼装上下文一轮下来少则几百毫秒多则好几秒。并发一上来模型接口还没崩自己的服务先扛不住了——会话状态丢了、工具调用结果重复拉取、限流计数错乱各种问题像约好了一样同时冒出来。我最早做 Agent 的时候也天真觉得“不就是调个大模型接口嘛能有多复杂”。结果第一次压测就被现实教育了50 个并发用户Agent 的平均响应时间从 1.2 秒飙到 8 秒以上日志里全是重复的工具调用和超时。排查下来发现问题根本不在模型本身而在于状态管理和缓存层完全缺失。Agent 是有“记忆”的多轮对话要记住上下文工具调用结果要复用用户会话要隔离这些如果每次都重新计算或者全塞在进程内存里单机还能凑合一旦多实例部署就彻底乱套。这就是 Redis 在 AI Agent 架构里真正的价值所在。它不是简单地“加个缓存提速”而是承担了三个关键角色会话状态的共享存储、工具调用结果的缓存层、以及并发控制的协调中心。你可以把 Agent 想象成一个记忆力很好但很忙的助理Redis 就是他手边那块随时能查、随时能写的白板——所有实例共享同一块白板谁写了什么大家都看得见不用每个人都把信息记在自己脑子里。这篇文章适合两类人看一类是正在搭建 AI Agent、被并发和状态问题折磨的开发者另一类是想搞清楚“Redis 在 Agent 里到底怎么用才不踩坑”的工程师。我会从实际架构出发把会话缓存、工具结果缓存、分布式锁、缓存失效策略这些核心问题一个个拆开讲配上能直接抄的代码和参数也会分享几个我踩过的坑。Redis 的安装、数据类型这些基础我不多废话重点放在 Agent 场景下的特殊用法和治理经验上。2. Agent 的会话状态到底该存什么、怎么存2.1 会话数据的结构设计别把整个对话历史一股脑塞进去很多人第一次做 Agent 会话存储图省事直接把整个 messages 数组序列化成 JSON 丢进一个 key 里。刚开始没问题但对话轮次一多这个 value 会膨胀到几百 KB 甚至上 MB每次读写都要全量序列化和反序列化延迟肉眼可见地上升。更麻烦的是并发写入——两个请求同时更新同一个会话后写的会把先写的覆盖掉上下文直接丢失。我的做法是分层存储。把会话数据拆成三部分热数据最近 N 轮对话比如最近 10 轮用 Redis 的 List 或 Stream 存储读写频繁需要快速访问。温数据会话的元信息比如用户 ID、会话创建时间、当前状态进行中/已完成、累计 token 消耗用 Hash 存储字段级更新避免全量覆盖。冷数据完整的历史对话定期归档到数据库或对象存储Redis 里只保留摘要或指针。具体结构可以这样设计# 会话元信息用 Hash支持字段级更新 HSET agent:session:{session_id} user_id u_12345 status active created_at 1716000000 token_used 1520 # 最近对话轮次用 ListLPUSH 新消息LTRIM 控制长度 LPUSH agent:session:{session_id}:messages {...} LTRIM agent:session:{session_id}:messages 0 19 # 设置过期时间避免僵尸会话堆积 EXPIRE agent:session:{session_id} 7200 EXPIRE agent:session:{session_id}:messages 7200这里有个细节值得说为什么用 List 而不是 String 存消息。List 的 LPUSH LTRIM 组合天然适合“只保留最近 N 条”的场景而且支持范围查询取最近 10 轮就是LRANGE key 0 9不用把整个历史都拉出来。String 存 JSON 数组的话每次追加都要读出来、解析、追加、再序列化写回去既慢又容易并发冲突。2.2 会话隔离与多租户key 的命名不是小事Agent 服务通常要同时服务多个用户、多个租户甚至多个 Agent 实例。如果 key 命名不规范很容易出现数据串号——A 用户的对话被 B 用户读到这在生产环境是严重事故。我习惯用冒号分隔的层级命名把租户、业务、实体类型、ID 都体现在 key 里{tenant}:{service}:{entity}:{id}:{sub_entity}比如t_001:agent:session:sess_abc123:meta t_001:agent:session:sess_abc123:messages t_001:agent:toolcache:weather:beijing:20240518这样做的好处有三个一是可读性强运维排查时一眼能看出这个 key 属于谁二是便于批量操作比如要清理某个租户的所有缓存用SCAN匹配t_001:agent:*就行注意生产环境别用KEYS后面会讲三是为分片预留空间如果将来要做 Redis Cluster可以在 key 里加 hash tag比如{t_001}:agent:session:...保证同一租户的数据落在同一个槽上。注意key 的长度也要控制。虽然 Redis 对 key 长度没有硬限制但过长的 key 会浪费内存而且在高并发下网络传输也是开销。一般建议控制在 100 字节以内把能简化的部分简化比如session可以缩写成sess但别缩到看不懂。2.3 会话过期与续期TTL 设多少才合理TTL 设置是个经验活。设太短用户聊到一半会话过期了体验极差设太长僵尸会话堆积内存被白白占用。我的经验值是根据业务场景分档场景类型建议 TTL续期策略一次性问答5-10 分钟不续期用完即弃多轮客服对话30-60 分钟每次交互后重置 TTL长期助理型 Agent7-30 天滑动过期 定期归档工具结果缓存1-24 小时按数据时效性设定续期的实现很简单每次读写会话时顺手EXPIRE一下import redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def touch_session(session_id, ttl3600): 每次交互后刷新会话过期时间 pipe r.pipeline() pipe.expire(fagent:session:{session_id}, ttl) pipe.expire(fagent:session:{session_id}:messages, ttl) pipe.execute()用 pipeline 把多个 EXPIRE 打包发送减少网络往返。别小看这个优化在高频交互场景下每次省几毫秒累积起来很可观。还有一个坑如果会话数据分散在多个 key 里一定要保证它们的 TTL 一致。我曾经遇到过 meta 过期了但 messages 还在的情况结果读会话时拿到一个没有元信息的孤儿消息列表程序直接报错。解决办法要么用 pipeline 统一设置要么用 Redis 7.0 以上的 hash 字段过期功能HEXPIRE把相关数据放在同一个 Hash 里。3. 工具调用结果缓存Agent 提速最狠的一刀3.1 哪些工具结果值得缓存哪些绝对不能缓存Agent 调用工具是耗时大户。一次天气查询、一次数据库检索、一次外部 API 调用动辄几百毫秒到几秒。如果同样的查询在短时间内重复出现缓存能直接把响应时间打到毫秒级。但不是所有工具结果都能缓存。我总结了一个判断标准可以缓存的幂等查询类天气、汇率、股票行情短时效、百科知识、静态配置计算密集类向量检索结果、复杂 SQL 查询结果、文档解析结果外部 API 类有明确时效性且调用成本高的接口绝对不能缓存的涉及用户隐私的操作转账、下单、修改密码实时性要求极高的库存扣减、秒杀库存有副作用的操作发消息、发邮件、写数据库判断的核心就一条同样的输入在缓存有效期内返回同样的输出且不产生任何副作用。满足这条就可以缓存。3.2 缓存 key 的设计怎么把工具参数变成唯一标识工具调用的缓存 key核心是把工具名 参数映射成一个唯一字符串。最直接的做法是拼 JSON 再哈希import hashlib import json def make_tool_cache_key(tool_name, params, versionv1): 生成工具调用缓存 key # 参数排序保证相同参数不同顺序生成相同 key normalized json.dumps(params, sort_keysTrue, ensure_asciiFalse) param_hash hashlib.md5(normalized.encode()).hexdigest()[:16] return fagent:toolcache:{version}:{tool_name}:{param_hash}这里有几个细节参数要排序{a:1,b:2}和{b:2,a:1}应该命中同一个缓存所以用sort_keysTrue。加版本号工具逻辑变更时旧缓存可能失效用版本号隔离避免脏数据。比如v1升级到v2新请求走新 key旧缓存自然过期。哈希截断MD5 取前 16 位足够既保证低碰撞率又控制 key 长度。中文处理ensure_asciiFalse让中文参数保持原样虽然哈希后看不出区别但调试时更友好。对于参数特别复杂的工具比如带嵌套对象的还可以考虑只取关键参数做 key。比如一个“搜索商品”工具参数有keyword、page、page_size、sort、filters其中filters是个大对象可以只取keyword page sort做 key牺牲一点精确性换取更高的命中率。这个取舍要看业务没有标准答案。3.3 缓存穿透、击穿、雪崩在 Agent 场景下的真实表现这三个经典问题在 Agent 场景下有自己的特点我一个个说。缓存穿透查询一个根本不存在的工具结果每次都打到后端。在 Agent 里这通常表现为用户反复问一个冷门问题或者工具参数组合特别偏。解决办法是缓存空结果但 TTL 设短一点def get_tool_result(tool_name, params): key make_tool_cache_key(tool_name, params) cached r.get(key) if cached is not None: if cached __NULL__: return None # 命中空结果缓存 return json.loads(cached) # 缓存未命中调用真实工具 result call_real_tool(tool_name, params) if result is None: # 空结果缓存 60 秒防止穿透 r.setex(key, 60, __NULL__) else: r.setex(key, 3600, json.dumps(result, ensure_asciiFalse)) return result缓存击穿某个热点 key 刚好过期大量并发请求同时打到后端。Agent 里最典型的就是热门问题——比如某个突发事件大量用户同时问同一个问题。解决办法是互斥锁重建只让一个请求去加载其他请求等待def get_tool_result_with_lock(tool_name, params, lock_timeout10): key make_tool_cache_key(tool_name, params) cached r.get(key) if cached is not None: return json.loads(cached) if cached ! __NULL__ else None lock_key f{key}:lock # 尝试获取锁NX 保证只有一个能拿到 if r.set(lock_key, 1, nxTrue, exlock_timeout): try: result call_real_tool(tool_name, params) r.setex(key, 3600, json.dumps(result, ensure_asciiFalse) if result else __NULL__) return result finally: r.delete(lock_key) else: # 没拿到锁短暂等待后重试读缓存 import time time.sleep(0.1) return get_tool_result_with_lock(tool_name, params, lock_timeout)缓存雪崩大量 key 在同一时间集中过期请求全部涌向后端。Agent 场景下如果批量预热了一批工具缓存又设了相同的 TTL就很容易触发。解决办法是TTL 加随机抖动import random def set_with_jitter(key, value, base_ttl3600): TTL 加 ±10% 随机抖动避免集中过期 jitter random.randint(-base_ttl // 10, base_ttl // 10) ttl base_ttl jitter r.setex(key, ttl, value)这个抖动看起来不起眼但在大规模缓存下能有效削峰。我实测过一个场景加了抖动之后后端峰值 QPS 从 3000 降到了 800 左右。4. 并发控制AI Agent 扛并发的 Redis 实战手段4.1 分布式锁工具调用的互斥与限流Agent 调用外部工具时有些工具是有速率限制的比如某个 API 每秒只允许 10 次调用。多实例部署下每个实例各自限流是没用的必须有一个全局协调者这就是 Redis 分布式锁的用武之地。最简单的实现是SET key value NX EXimport uuid def acquire_lock(lock_name, timeout10): 获取分布式锁返回锁标识或 None token str(uuid.uuid4()) acquired r.set(fagent:lock:{lock_name}, token, nxTrue, extimeout) return token if acquired else None def release_lock(lock_name, token): 释放锁用 Lua 脚本保证原子性 lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end r.eval(lua, 1, fagent:lock:{lock_name}, token)释放锁必须用 Lua 脚本因为“判断是不是自己的锁”和“删除锁”这两步必须是原子的。如果先 GET 再 DEL中间锁过期被别人拿走了你就会误删别人的锁。但说实话单实例 Redis 锁在 Redis 主从切换时是有风险的——主节点写入锁后还没同步到从节点就宕机了从节点升主后锁就丢了。如果业务对锁的可靠性要求极高可以考虑 Redlock 算法多个独立 Redis 实例投票但 Redlock 本身也有争议实现复杂且性能下降。我的建议是大多数 Agent 场景用单实例锁 合理超时就够了真正需要强一致的地方应该用数据库的唯一约束或事务来兜底而不是把宝全押在 Redis 锁上。4.2 限流器滑动窗口与令牌桶的 Redis 实现Agent 服务需要限流的地方很多单用户请求频率、单工具调用频率、全局 QPS 上限。Redis 实现限流有两种常用方案。固定窗口计数最简单但边界问题明显——比如限制每分钟 100 次用户在 0:59 发 100 次1:00 又发 100 次实际两秒内发了 200 次。Agent 场景下这种突发很容易打垮后端。滑动窗口更平滑用 ZSET 实现import time def sliding_window_limit(user_id, limit100, window60): 滑动窗口限流返回 True 表示允许 key fagent:ratelimit:{user_id} now time.time() pipe r.pipeline() # 移除窗口外的记录 pipe.zremrangebyscore(key, 0, now - window) # 统计窗口内请求数 pipe.zcard(key) # 添加当前请求 pipe.zadd(key, {str(now): now}) # 设置过期避免冷用户占用内存 pipe.expire(key, window) results pipe.execute() count results[1] return count limit这个实现每次请求要执行 4 个命令用 pipeline 打包后一次网络往返性能可以接受。如果限流粒度更粗比如全局 QPS可以用令牌桶用 Lua 脚本在 Redis 里维护令牌数和上次填充时间原子地取令牌。提示限流的 key 一定要设过期时间。我见过有人忘了设结果每个用户都留一个 ZSET几个月后内存爆了。滑动窗口的 key 过期时间设成窗口大小就行窗口外的数据反正也没用。4.3 并发下的会话一致性读改写竞态怎么破Agent 处理一轮对话通常要经历“读会话 → 调模型 → 调工具 → 写会话”这个过程。如果同一个会话有两个请求并发进来比如用户手快连发两条消息就会出现读改写竞态两个请求都读到旧状态各自处理后写回后写的覆盖先写的一条消息就丢了。解决办法有两个思路。一是串行化用分布式锁把同一会话的请求串起来def handle_message(session_id, message): lock_token acquire_lock(fsession:{session_id}, timeout30) if not lock_token: raise Exception(会话正忙请稍后重试) try: session load_session(session_id) # ... 处理逻辑 save_session(session_id, session) finally: release_lock(fsession:{session_id}, lock_token)二是乐观锁用版本号或 WATCH/MULTI 实现 CASdef save_session_optimistic(session_id, session, expected_version): 乐观锁保存版本不匹配则失败 key fagent:session:{session_id}:meta with r.pipeline() as pipe: try: pipe.watch(key) current pipe.hget(key, version) if current and int(current) ! expected_version: pipe.unwatch() return False pipe.multi() pipe.hset(key, mapping{version: expected_version 1, updated_at: int(time.time())}) pipe.execute() return True except redis.WatchError: return False串行化简单粗暴但会降低并发度乐观锁并发度高但需要重试逻辑。我的经验是会话级别的操作用串行化更省心因为 Agent 处理本身就不快串行化的性能损失可以接受而工具缓存写入这种高频操作用乐观锁或无锁设计更合适。5. 缓存治理上线之后才是真正的考验5.1 内存监控与淘汰策略别等 OOM 了才想起来看Redis 内存爆掉是 Agent 服务最常见的线上事故之一。工具缓存、会话数据、限流记录样样都吃内存不监控就是定时炸弹。首先要设置maxmemory和淘汰策略。Agent 场景我推荐allkeys-lru或volatile-lrumaxmemory 4gb maxmemory-policy allkeys-lruallkeys-lru对所有 key 做 LRU 淘汰适合缓存和会话混存的场景volatile-lru只淘汰设了过期时间的 key如果你把重要数据设了持久化不设 TTL用这个更安全。千万别用 noeviction内存满了之后所有写操作直接报错Agent 会全面瘫痪。监控方面这几个指标必须盯指标命令关注点内存使用INFO memoryused_memory 接近 maxmemory 就要警惕命中率INFO statshit_rate 低于 80% 说明缓存设计有问题淘汰数INFO statsevicted_keys 持续增长说明内存不够慢查询SLOWLOG GET超过 10ms 的命令要优化连接数INFO clientsconnected_clients 突增可能是连接泄漏我一般会把这些指标接到监控面板上设好告警阈值。特别是evicted_keys如果它开始持续增长说明缓存正在被大量淘汰命中率会下降后端压力会上升得赶紧扩容或优化 key 设计。5.2 大 key 与热 keyAgent 缓存里的隐形杀手大 key是指 value 特别大的 key比如一个存了几万条消息的 List或者一个几 MB 的 JSON String。大 key 的危害在于读写慢、网络传输慢、删除时会阻塞 RedisRedis 4.0 之前用DEL删大 key 会卡住之后可以用UNLINK异步删除。排查大 key 用redis-cli --bigkeys或者用MEMORY USAGE key看单个 key 的内存占用。Agent 场景下最容易出大 key 的地方就是会话消息列表一定要用LTRIM控制长度别让它无限增长。热 key是指访问频率极高的 key比如某个热门问题的缓存。热 key 会导致单个 Redis 节点 CPU 飙升成为瓶颈。排查热 key 可以用redis-cli --hotkeys需要 maxmemory-policy 是 lru 类或者自己在客户端埋点统计。热 key 的解决办法有几种本地缓存在应用进程内再缓存一份减少 Redis 访问、key 拆分把hotkey拆成hotkey:1、hotkey:2分散到不同节点、读写分离热 key 走从节点读。Agent 场景下我常用本地缓存 短 TTL 的组合比如热门工具结果在进程内缓存 5 秒能挡掉大部分重复请求。5.3 缓存与数据库的一致性Agent 记忆的持久化策略Redis 是内存数据库虽然可以持久化RDB/AOF但不能把 Redis 当作唯一的数据存储。Agent 的会话记忆、用户偏好这些重要数据必须有数据库兜底。我的做法是Cache-Aside 模式读的时候先查 Redis没有再查数据库并回填写的时候先写数据库再删除 Redis 缓存注意是删除不是更新避免并发写导致脏数据。def get_session(session_id): # 先查缓存 cached r.get(fagent:session:{session_id}) if cached: return json.loads(cached) # 缓存未命中查数据库 session db.query_session(session_id) if session: r.setex(fagent:session:{session_id}, 3600, json.dumps(session)) return session def update_session(session_id, data): # 先写数据库 db.update_session(session_id, data) # 再删缓存 r.delete(fagent:session:{session_id})这里有个经典问题先删缓存还是先写数据库。两种顺序都有并发问题业界没有完美方案。我的选择是“先写库再删缓存”因为这种顺序下最坏情况是缓存里短暂存在旧数据但不会出现“缓存已删、库还没写”导致读到旧库数据的更糟情况。如果业务对一致性要求极高可以用延迟双删写库后删一次缓存延迟几百毫秒再删一次覆盖掉可能的并发读回填。对于 Agent 的长期记忆比如用户画像、历史偏好我建议异步持久化会话过程中数据主要在 Redis 里流转定期比如每 5 分钟或会话结束时批量同步到数据库。这样既保证了性能又不会丢数据。6. 几个我踩过的坑和对应的解法6.1 序列化选型JSON 不是万能的一开始我所有缓存都用 JSON 序列化简单直观。但后来发现两个问题一是性能JSON 序列化/反序列化在数据量大时很吃 CPU二是类型丢失JSON 不支持二进制、日期等类型存进去再取出来就变样了。后来我根据数据类型分开处理会话元信息用 Hash 存储字段直接是字符串不需要序列化。工具结果如果结构简单用 JSON如果包含二进制或复杂类型用MessagePack或PicklePython 场景。大对象考虑压缩后再存比如 gzip base64牺牲一点 CPU 换内存。import msgpack def set_msgpack(key, value, ttl3600): packed msgpack.packb(value, use_bin_typeTrue) r.setex(key, ttl, packed) def get_msgpack(key): data r.get(key) if data: return msgpack.unpackb(data, rawFalse) return NoneMessagePack 比 JSON 快 2-5 倍体积小 30% 左右在 Agent 这种高频读写的场景下收益明显。但可读性差调试时得用工具解码所以我在开发环境用 JSON生产环境用 MessagePack通过配置切换。6.2 连接池配置别让连接数成为瓶颈Redis 客户端连接池配置不当会导致连接数暴涨或请求排队。Python 的 redis-py 默认连接池没有上限高并发下会创建大量连接把 Redis 的maxclients撑爆。我的配置经验pool redis.ConnectionPool( hostlocalhost, port6379, max_connections50, # 根据并发量调整 socket_timeout2, # 读写超时别设太长 socket_connect_timeout1, # 连接超时 retry_on_timeoutTrue, # 超时重试 health_check_interval30 # 定期健康检查 ) r redis.Redis(connection_poolpool)max_connections设多少合适一个经验公式是并发请求数 × 1.5。比如你的 Agent 服务峰值 100 并发每个请求平均持有连接 10ms那理论上 100 × 0.01 1 个连接就够但考虑到突发和阻塞设 50-100 比较稳妥。设太小会排队设太大浪费资源且可能触发 Redis 的连接数限制。socket_timeout特别重要。Agent 调用 Redis 如果卡住没有超时的话线程会一直挂着最终拖垮整个服务。我一般设 2 秒超过就报错走降级逻辑。6.3 缓存失效时的降级Redis 挂了 Agent 还能不能跑这个问题很现实Redis 是单点或者主从万一挂了Agent 服务是直接不可用还是能降级运行我的设计原则是Redis 故障时核心功能可用非核心功能降级会话数据Redis 挂了降级到数据库读写性能下降但功能可用。工具缓存Redis 挂了直接跳过缓存调用真实工具慢但能用。限流Redis 挂了降级到本地限流单机限流虽然不精确但能防止打垮后端。分布式锁Redis 挂了降级到数据库锁或直接放行根据业务容忍度。实现上就是包一层 try-except捕获 Redis 异常后走降级逻辑def safe_redis_call(func, fallback, *args, **kwargs): try: return func(*args, **kwargs) except (redis.ConnectionError, redis.TimeoutError) as e: logger.warning(fRedis 调用失败走降级: {e}) return fallback(*args, **kwargs)这个降级逻辑一定要在上线前就测试过别等真出故障了才发现降级代码有 bug。我一般会用工具模拟 Redis 不可用跑一遍全流程确保降级路径是通的。6.4 那些年我误用的 Redis 命令最后分享几个命令层面的坑KEYS *生产环境绝对禁用。它会阻塞 Redis 直到遍历完所有 keykey 多了直接卡死。要用SCAN游标分批遍历def scan_keys(pattern, batch100): cursor 0 while True: cursor, keys r.scan(cursor, matchpattern, countbatch) for key in keys: yield key if cursor 0: breakDEL大 key删除大 key 会阻塞用UNLINK异步删除。FLUSHALL手滑执行一次整个 Redis 清空。生产环境应该用rename-command禁用或重命名这个命令rename-command FLUSHALL rename-command FLUSHDB rename-command KEYS HGETALL大 Hash如果 Hash 字段很多HGETALL会一次性拉取所有字段阻塞且占带宽。用HSCAN分批读或者只读需要的字段HMGET。这些坑我都真实踩过尤其是KEYS当年在测试环境跑了一次Redis 卡了十几秒所有请求超时。从那以后我在配置文件里直接把KEYS命令禁了逼着自己用SCAN。7. 关于 Agent 缓存治理的一点个人体会做了几个 Agent 项目之后我越来越觉得 Redis 在 Agent 架构里的角色被低估了。很多人把它当成一个简单的“加速层”但实际上它承担的是状态协调的职责——多实例之间怎么共享会话、怎么协调工具调用、怎么控制并发这些问题的答案都在 Redis 里。我的核心体会是缓存设计要从业务的一致性要求出发而不是从技术便利出发。哪些数据可以容忍短暂不一致哪些必须强一致哪些可以异步持久化想清楚这些再决定用什么数据结构、设什么 TTL、要不要加锁。技术选型是结果不是起点。另外一个教训是监控和治理要前置。别等线上出问题了才去查大 key、热 key、内存使用。上线前就把监控搭好把淘汰策略配好把危险命令禁掉这些准备工作花不了多少时间但能省掉后面无数个加班的夜晚。最后说个具体的Agent 的会话数据我现在的做法是Redis 存热数据 数据库存全量 定期归档冷数据三层配合。Redis 里只保留最近 20 轮对话和会话元信息TTL 设 2 小时数据库存完整历史超过 30 天的数据归档到对象存储。这样 Redis 内存可控查询性能也好数据也不会丢。这套方案在几个项目里跑下来单实例支撑几千并发没什么压力内存占用也稳定在可控范围内。
返回列表