ARTICLE DETAIL

资讯详情

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

AI Agent缓存设计实战:抗并发、会话记忆与线上治理

AI Agent缓存设计实战:抗并发、会话记忆与线上治理 上个月我负责的 Agent 服务在群里被同事连戳好几下结果 Redis 直接被干到 CPU 100%Lettuce 客户端疯狂报command timed out原本两秒能出结果的一轮对话硬生生拖到半分钟才恢复。排查到最后我发现问题根本不在 Redis 本身而是我从一开始就把 AI Agent 的缓存需求当成普通 Web 接口的 KV 缓存来做了。这篇文章不聊那些抄来抄去的 Redis 基础命令专门讲 Agent 项目里缓存到底该怎么设计、怎么扛并发、怎么治理线上缓存。内容围绕我自己的实操经历展开适合正在做 Agent 应用、尤其是准备上多实例和会话记忆的开发者。你会看到为什么 Agent 比传统服务更依赖缓存、具体缓存哪些数据、TTL 怎么设才不翻车、Lettuce 连接池怎么调以及线上缓存治理的几个典型事故复盘。1. AI Agent 场景下的缓存和 Web 项目的缓存根本不是一回事1.1 Agent 比普通接口更依赖缓存Token 成本和延迟的双重压力传统 Web 接口的缓存目标很单一少查一次数据库、少算一次逻辑接口响应快一点。但到了 AI Agent 这里缓存的战略价值完全不同——它直接关系到两件事你的钱包和用户的耐心。LLM 调用是按 Token 计费的而且单次推理的延迟是以秒为单位计的。如果一个 Agent 每次收到用户消息都要把完整的历史对话重新塞进 Prompt再让模型重新理解一遍那成本简直是在烧钱。我在项目里做过一个简单的对比一个带工具调用的 Agent单轮交互平均要消耗 2 到 5 万 Token按当前主流模型的价格算一次完整任务可能花掉几毛到几块钱。如果同样的工具查询结果每次都重新执行一遍费用翻一倍不止。延迟也一样。用户问一句今天北京天气适合穿什么Agent 内部可能要先调天气 API、再让模型基于结果做判断。如果天气信息缓存一小时命中时整个流程直接少一次外部 HTTP 调用响应时间从可能的三四秒压到一秒以内。用户体验的差异在这种高频小需求上非常明显。所以我的结论是Agent 的缓存不只是少查一次数据库而是少调一次模型、少执行一轮工具。这个认知上的转变很重要它会直接影响你设计缓存的粒度。普通接口缓存可以粗糙一点Agent 的缓存必须精确到哪类数据值得缓存、缓存多久、失效后怎么重建。1.2 内存缓存为什么撑不住多实例和动态扩缩容是现实很多人在项目初期图省事把 Agent 的会话记忆直接放在进程内一个全局 Dict 或者 ConcurrentHashMap 里。单机 Demo 跑起来确实很爽记忆秒读不需要任何序列化。但一旦你的 Agent 要接真实流量这套方案立刻失效。现在的 Agent 服务基本都是无状态多副本部署前面挂负载均衡请求随机分发到不同实例。用户第一轮消息落到了实例 A记忆存在 A 的内存里第二轮请求被分到了实例 BB 翻遍整个内存也找不到上一轮聊了什么只能让用户重新说一遍甚至干脆报错。这还只是最浅层的问题。更隐蔽的问题是动态扩缩容。流量一上来副本从 2 个扩到 10 个新实例启动时内存是空的旧实例里的记忆只有一部分用户能命中。扩容不仅没解决问题反而把缓存命中率稀释了。我做过的另一个真实项目里就因为这个问题每次扩容后的一段时间内Agent 的响应质量会明显下降因为大量请求都要重新走完整的 LLM 链路。所以进程内缓存不是不能用而是只能用在丢了也无所谓的数据上比如热点工具结果的本地二级缓存。凡是涉及会话记忆、用户状态、任务状态这类数据必须放进 Redis 这样的共享状态层。这不是架构洁癖是分布式场景下的硬需求。1.3 读写模型的变化Agent 状态不是读多写少传统缓存假设数据是读多写少所以缓存里放一份、数据库里放一份缓存的职责是加速读。但 Agent 的会话状态完全是另一种模式——每一轮对话结束状态都要被追加修改下一轮又立刻要读到最新的状态。这是典型的读改写模型。这意味着你不能把 Redis 仅仅当成一个性能加速层而要把它当成可恢复的状态存储。一旦 Redis 里的会话状态丢失Agent 就失去了上下文整个任务可能要从头开始。缓存命不中不是多一次查询的问题而是用户对话断片的问题。我现在的做法是把 Agent 运行时的状态分成两类。一类是可重建的派生数据比如工具调用的结果缓存丢了最多重新调一次工具影响可控另一类是不可重建的原始状态比如会话历史、用户意图、任务进度这类数据虽然也会放在 Redis 里但必须当作持久状态来对待要有备份、要有恢复策略绝不能只设一个短 TTL 了事。2. Agent 到底该缓存什么记忆、工具结果和成本账2.1 会话记忆窗口用 Redis List 管好最近 N 轮Agent 的记忆管理是缓存设计的第一个硬骨头。很多人一上来就想把整个对话历史都存进 Redis存着存着就发现上下文太长Prompt 塞不下LLM 调用费暴涨Redis 内存也吃紧。这里必须建立起滑动窗口思维。我的实践是用 Redis List 结构按会话维度存储消息记录每个会话一个 Key比如agent:{app_id}:session:{session_id}:messages。每次用户和 Agent 各产生一条消息就RPUSH进去然后立刻LTRIM只保留最近 N 轮。N 的大小取决于你用的模型上下文窗口和 Prompt 设计我一般取 10 到 20 轮既能保持对话连贯性又不会撑爆模型输入。下面是 Python 侧的写入逻辑这段代码我线上跑了大半年非常稳import redis import json r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def append_message(session_id: str, role: str, content: str, max_turns: int 20): key fagent:demo:session:{session_id}:messages msg json.dumps({role: role, content: content, ts: time.time()}, ensure_asciiFalse) pipe r.pipeline() pipe.rpush(key, msg) # 只保留最近 max_turns*2 条消息一问一答算两条 pipe.ltrim(key, -(max_turns * 2), -1) # 滑动过期每次写入都刷新 TTL用户 30 分钟后再回来记忆还在 pipe.expire(key, 1800) pipe.execute() def read_messages(session_id: str): key fagent:demo:session:{session_id}:messages return r.lrange(key, 0, -1)用RPUSH追加用LTRIM控制长度用EXPIRE做滑动过期。这样既保证了记忆的连续性又不会让 Redis 里堆积垃圾数据。这里有个细节值得注意LTRIM 的第二个参数用负数-21表示从末尾往回数 21 个配合管道让追加和裁剪在同一个连接里原子完成避免并发时刻多条消息导致窗口不一的问题。这种结构的好处是读取时天然有序按时间顺序排列直接丢给 LLM 组成上下文数组就行。我见过有人用 String 类型把整个对话 JSON 序列化成一个 Key每次更新都整体读出来再写回去并发一高就丢数据纯粹给自己找罪受。2.2 工具调用结果缓存幂等是前提参数哈希是钥匙第二个值得缓存的是工具调用的结果。Agent 的核心能力就是调用工具但工具调用往往有成本外部 API 有配额、有延迟数据库查询有压力。如果多个用户同时问同类问题或者同一个任务里模型反复尝试同一个工具重复执行就是纯浪费。不过不是所有工具结果都能缓存。前提是幂等且结果可复用。比如查天气、查列车时刻表、查文档片段这些是天然适合缓存的但创建订单发送消息这类有副作用的操作绝不能缓存结果。我在项目里给工具定义了三个属性来决定缓存策略幂等性、时效性、结果体积。实现方式也不复杂。每个工具定义一个缓存 Key格式是agent:{app_id}:tool:{tool_name}:{param_hash}其中param_hash是请求参数的规范化哈希。规范化意味着参数顺序要先排序再哈希否则city北京date今天和date今天city北京会算出两个 Key白白浪费缓存空间。import hashlib import json def tool_cache_key(tool_name: str, params: dict, version: str v1): # 参数排序后序列化保证相同语义的参数命中同一个 key canonical json.dumps(params, sort_keysTrue, ensure_asciiFalse) h hashlib.md5(canonical.encode()).hexdigest() return fagent:demo:tool:{tool_name}:{version}:{h} def get_tool_result(tool_name: str, params: dict, ttl: int): key tool_cache_key(tool_name, params) cached r.get(key) if cached: return json.loads(cached) # 未命中执行真实工具调用 result call_tool(tool_name, params) r.setex(key, ttl, json.dumps(result, ensure_asciiFalse)) return resultTTL 的设置按数据特性分级天气预报类缓存 30 分钟新闻资讯类缓存 5 到 10 分钟静态文档类可以放到 24 小时。千万不要一刀切全部设成同样的过期时间我在后面缓存治理部分会具体讲这个坑。2.3 限流、计数与 Token 预算INCR 和滑动窗口的经典用法除了业务数据Agent 的运维面也需要 Redis。最典型的是限流和 Token 预算控制。一个 Agent 服务上线后你必然要面对几个问题单个用户每分钟最多能请求多少次外呼模型 API 的费用有没有失控某个 Key 的并发是不是把下游工具打爆了这些场景不需要复杂结构Redis 的INCR配合EXPIRE就是最可靠的计数器。按固定窗口限流的话可以这么写def rate_limit(user_id: str, limit: int 10, window: int 60) - bool: key fagent:demo:ratelimit:{user_id}:{int(time.time() // window)} count r.incr(key) if count 1: r.expire(key, window 1) return count limit固定窗口有个经典问题在窗口临界点请求量可能翻倍。如果 Agent 要面对严格的限流要求我建议用 Lua 脚本实现滑动窗口在 Redis 内部用 ZSet 存储请求时间戳计算窗口内的请求数。下面是线上可用的脚本local key KEYS[1] local now tonumber(ARGV[1]) local window tonumber(ARGV[2]) local limit tonumber(ARGV[3]) local member ARGV[4] redis.call(ZREMRANGEBYSCORE, key, 0, now - window) local count redis.call(ZCARD, key) if count limit then redis.call(ZADD, key, now, member) redis.call(PEXPIRE, key, window) return 1 else return 0 end这段脚本做的事情很简单先把窗口外的旧记录清掉再统计当前窗口内数量没超就写入当前请求并刷新过期时间。这里强调一下这类涉及判断后写入的操作一定要放 Lua 或事务里千万不要先ZADD再判断因为并发情况下后判断的请求会顶掉先判断的。3. 缓存策略与失效模型缓存失效才是连环坑的起点3.1 TTL 设计给记忆设 TTL 之前先搞清楚用户在等什么一次线上事故让我彻底记住了这个教训。当时我图省事给会话记忆 Key 设置了固定 TTL 10 分钟。结果用户聊到第 10 分钟时刚好回去回复上一条消息Agent 完全不记得前面在聊什么直接反问请问您刚才说的是什么。用户的体验从智能助手瞬间降级成健忘症客服。从那以后我给自己定了一个规矩所有会话记忆类 Key 一律使用滑动过期每次读取或写入都把 TTL 刷新到预设的上限值。比如预设 30 分钟不活跃就清理但用户只要每 20 分钟说一句话这个会话就永远不会断。这背后的逻辑很简单会话记忆的时效性不是从创建时算起而是从最后一次活跃算起。用代码表示就是每次读改写时都带上EXPIRE操作。前面会话记忆的例子已经展示过了这里我再强调一句这只是 Redis 的设计习惯但这个习惯在 Agent 场景里直接决定了用户体验认真程度要提到最高。工具结果的 TTL 就要区分对待了。我的分法是这样动态数据实时汇率、库存、排队人数TTL 控制在 30 秒到 5 分钟半静态数据天气预报、新闻标题5 到 30 分钟静态资料文档片段、产品信息1 到 24 小时。看到这里你可能会说那为什么不干脆都设 5 分钟因为工具每次调用都是有成本的同一份文档你让模型重新读十遍每次都是实打实的 Token 消耗。这份成本应该用更长的 TTL 去对冲。3.2 序列化方案从服务崩溃到跨语言重构全是同一个坑缓存治理里最容易翻车的地方是序列化方案选错。我用 Java 写过一段时间的 Agent 服务见过最典型的场景是RedisTemplate默认用 JDK 序列化往 Redis 里塞对象存进去是一串\xAC\xED\x00\x05t...的二进制。这套东西单机跑没问题一上规模就爆雷。为什么雷会爆第一JDK 序列化体积大。同样一个对象JSON 序列化后占 200 字节JDK 序列化能膨胀到 1KB 以上。缓存数据量一大Redis 内存噌噌往上涨内存到顶就开始频繁淘汰命中率断崖式下跌。第二JDK 序列化的反序列化强依赖类结构。你改了 Java 类的字段名或者包路径再读旧缓存直接抛ClassNotFoundException线上一次发布可能就把整个缓存层打崩。第三跨语言完全不可读。后来我们把部分模块从 Java 迁到 Rust 或 PythonJDK 序列化出来的数据对方根本没法解析只能全部清空再重构。所以我的建议是无论技术栈是什么缓存里存统一用 JSON 字符串或者更紧凑的 MessagePack、Protobuf。多语言 Agent 架构逐渐成为趋势今天你用 Java 写明天可能有个组件用 Rust 重写我在迁移过程中深有体会统一序列化是避免未来重构成本的唯一正解。如果是 Java 侧的 Spring Boot 项目下面的配置可以直接抄Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 使用 StringRedisSerializer 处理 key避免出现二进制乱码 template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); // value 统一用 GenericJackson2JsonRedisSerializer GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; }存储端就一个要求让人眼能看懂、让机器能跨语言解析。JSON 是最大公约数绝大多数场景下够用超大 Value 再考虑二进制协议。3.3 Key 设计与版本号缓存重构的第一件事是给 Key 换代经历了几次线上缓存事故后我把 Agent 项目的 Key 规范总结成了一句话没有版本号的缓存 Key都是埋给自己的雷。之前我在项目里改了一个工具返回的数据结构字段从location改成了address旧的缓存数据还在新代码读出来字段缺失Agent 在部分用户那里直接崩。问题就出在缓存 Key 没有版本概念新旧数据混用。现在的 Key 格式统一是agent:{app_id}:{scope}:{entity}:{version}:{id}具体例子agent:demo:session:v2:user_123:messages、agent:demo:tool:v3:weather:f1a2b3c4。每次发布涉及缓存数据格式变化时把版本号从v2升到v3旧 Key 自然过期不会和新数据混在一起。这比发布时手动清理缓存安全得多因为人总会忘记执行清理脚本。还有一个细节容易被忽略如果你要用MGET、MSET、事务等跨 Key 操作在 Redis 集群模式下要注意把相关 Key 设计成带相同 hash tag 的形式比如{session:user_123}:messages和{session:user_123}:context。Redis 集群对带{}的 Key 会按花括号里的内容计算哈希槽这样两个 Key 就能落在同一节点上跨 Key 操作才不会被CROSSSLOT报错挡住。4. 扛并发的核心连接管理、Pipeline 与分布式锁4.1 一次 redis command timed out 的完整根因复盘回到开头那个事故。我当时处理的是一个知识库问答 Agent用户量不大但每个问题都会触发大量并发查询。线上突然开始报错日志里刷满了redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException排查的第一直觉是 Redis 服务器挂了但看监控CPU 才 20%内存正常。后来用redis-cli看info clients发现连接数已经飙到几千瞬时 QPS 高得离谱。再看服务端日志发现连接池参数是默认值最大连接数只有 8。问题一下就清楚了。Lettuce 客户端底层是 Netty 异步模型默认连接池很小一旦请求量超过连接池处理能力请求就会在客户端排队排到超过commandTimeout默认 60 秒就会直接超时。这不是 Redis 扛不住是客户端把请求堵死了。我当时的修复方案是调大连接池和超时时间并给关键操作加上连接池监控spring: data: redis: timeout: 3s lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 2s调完之后同样的流量下毛刺消失。这里要提醒一句连接池不是越大越好。每个连接在 Redis 端都有内存开销连接数翻倍内存占用也翻倍。32 是我当时压测下来的折中值你的项目要根据实际 QPS 和平均命令耗时来压测调整。还有一个更关键的经验Redis 操作一定要做超时降级。我在线上加了一个开关当 Redis 命令执行超过 500ms 时工具调用直接按缓存未命中处理继续走真实逻辑而不是卡住整个请求。Agent 是有状态交互一个环节卡死会拖垮整轮对话降级永远比报错好。4.2 Pipeline、Lua 与事务批量操作的正确姿势Agent 项目里有个高频场景一轮对话要同时写入用户消息、助手消息、更新会话状态、刷新 TTL。如果一条一条发命令每次都是一次网络 RTT时间全浪费在握手上了。用pipeline()批量发送能把多个命令合并在一次 RTT 里收益非常明显。pipe r.pipeline(transactionFalse) pipe.rpush(msg_key, user_msg_json) pipe.rpush(msg_key, assistant_msg_json) pipe.ltrim(msg_key, -40, -1) pipe.expire(msg_key, 1800) pipe.incr(agent:demo:usage:today) pipe.execute()注意transactionFalse表示不启用事务只做管道批量发送。如果你需要多条命令原子执行比如判断存在才写入比较后更新则必须用 Lua 脚本或者transactionTrue。我在项目里用得最多的是 Lua 脚本因为事务在集群模式下要保证 Key 在同一个槽位限制多而 Lua 脚本天然原子执行还省网络交互。下面是一个我用来做会话状态乐观锁更新的 Lua 脚本。场景是这样两个并发请求同时修改同一个会话状态后写覆盖先写导致丢数据。通过附带版本号来保证谁新谁生效local key KEYS[1] local expected_version tonumber(ARGV[1]) local new_version tonumber(ARGV[2]) local new_data ARGV[3] local old_data redis.call(GET, key) local old_version 0 if old_data and old_data ~ then -- 假设数据是 JSON第一行存版本号{version:1,data:...} local ok, parsed pcall(function() return cjson.decode(old_data) end) if ok then old_version parsed.version or 0 end end if old_version expected_version then redis.call(SET, key, cjson.encode({version new_version, data cjson.decode(new_data)})) redis.call(PEXPIRE, key, 1800000) return 1 else return 0 end客户端调用脚本后如果返回 0说明版本冲突需要重新拉取最新状态再合并修改。这套逻辑能把并发丢状态的概率压到非常低。我强烈建议把所有涉及先读后写、且要求原子性的缓存操作都放进 Lua 里别自己用GET SET拼拼出来的代码在并发下基本都会出问题。4.3 Agent 里什么时候才该上分布式锁不是每个锁都必要分布式锁是 Redis 话题里被问烂了的东西但在 Agent 场景里它并不是万能药。我的原则很简单能通过设计避免共享可变状态就别上锁必须上锁时用 Lua 实现白锁过期和防误删。Agent 里最常见的锁场景是同一会话的串行化执行。当一个用户同时开了两个 Tab 操作同一个 Agent 会话时两个请求可能同时修改会话状态导致上下文错乱。这种情况可以用会话级分布式锁保证同一时间只有一个请求在改某个 session 的状态def acquire_session_lock(session_id: str, acquire_timeout_ms: int 3000): key fagent:demo:lock:session:{session_id} token uuid.uuid4().hex # SET NX PX 原子地抢占锁 ok r.set(key, token, nxTrue, px5000) if ok: # 抢锁成功返回释放锁用的 token return token return None def release_session_lock(session_id: str, token: str): key fagent:demo:lock:session:{session_id} # 释放锁时对比 token防止把别人的锁删掉 lua if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end r.eval(lua, 1, key, token)这里用 UUID 作唯一标识释放时用 Lua 校验是不是自己持有的锁防止超时后锁被别的线程续上又被误删。锁的超时时间要足够长以覆盖单轮 Agent 操作但不能太长否则一个慢任务会把整个用户卡死。我给会话锁定的是 5 秒过期因为单轮 LLM 调用加工具执行我实测一般不会超过 4 秒超时就报错让用户重试。再说一次分布式锁要克制使用。我见过一个项目给每个工具调用都加锁结果 Agent 还没开始思考光排队等锁就用掉了一半时间。加锁之前先问自己三个问题这里真的会发生并发吗并发导致的结果是错的吗有没有通 Design 避免三个都属实才值得上锁。5. 线上缓存治理监控、降级与事故复盘5.1 用命中率和内存增速发现缓存设计缺陷缓存上线后不是完事大吉而是要持续盯治理指标。我最看重的指标有三个缓存命中率、内存增速、大 Key 数量。命中率最能反映缓存设计是否合理。Agent 场景里工具结果缓存的命中率一般要做到 70% 以上会话记忆命中率则应该在 99% 以上因为滑动窗口和版本化设计按理说不该 miss。如果发现某个工具缓存的命中率不到 50%那说明要么 Key 设计有问题比如参数没有规范化导致同一个请求算了多个 Key要么 TTL 设置太短缓存刚写进去过几分钟就过期了。用redis-cli定期扫一下热点命令配合info stats里的keyspace_hits和keyspace_misses就能算出来。内存增速异常则多半是大 Value 或序列化膨胀问题。我在一个项目里发现 Redis 内存一周涨了 5GB用redis-cli --bigkeys扫完才发现有个大 JSON Value 存了 800KB 的工具响应几百个用户各自缓存一份内存瞬间爆掉。后来的规则很简单单 Value 超过 100KB 的内容就不直接存原数据而是存引用指针数据本身落到对象存储需要时再回源读取。5.2 穿透、击穿、雪崩在 Agent 场景的变体缓存三大经典问题穿透、击穿、雪崩在 Agent 场景各有变体处理方式也不能照搬传统 Web 经验。缓存穿透在 Agent 里的典型场景是查询一个不存在的会话 ID 或工具参数。比如用户手动拼接了一个不存在的 session_idAgent 每次都要去 Redis 和底层存储各查一遍。传统做法是缓存空值给不存在的数据也写一个短 TTL 的空壳。这个在 Agent 场景也能用但要注意如果下游确实有不存在的业务含义比如订单号错了不能缓存空结果误导用户就需要加布隆过滤器兜底。缓存击穿对应的是热点知识。当一个热门知识片段被同时几百个会话请求而它恰好刚过期所有请求同时回源重建瞬间把 LLM 调用或数据库压垮。我的处理办法是互斥重建缓存未命中时先抢锁抢到锁的请求去执行重建并回填缓存其他请求短暂等待后直接读缓存。这和前面分布式锁的用法一致。缓存雪崩在 Agent 场景里多了一个变体很多 Agent 服务喜欢用同样的 TTL 值导致大量 Key 在同一时刻集体过期。解决办法很简单给 TTL 加一个随机抖动原来设 300 秒的在 240 到 360 秒之间随机取。这样过期时间错峰避免了集中回源。多一层保险是给核心工具结果增加本地二级缓存即使 Redis 里有 N 个 Key 同时过期本地 JVM/Python 进程里也有一份最近的数据顶着能让回源压力降好几个量级。5.3 一场序列化事故的复盘与补救聊一个真实的事故当时我负责的 Java Agent 线上出现了一个诡异的现象Redis 内存持续增长但业务上并没有增加多少用户。用可视化客户端看数据发现里面存了一堆二进制乱码 Value用工具都解析不出来。排查过程是这样的先看代码定位写入方发现是我们升级了工具调用逻辑把原本应该用 JSON 序列化存入 Redis 的ToolResponse对象在某个分支里走了默认的 JDK 序列化。类里的嵌套字段在反序列化时还需要额外的类信息一旦原始类结构变化缓存全部不可读。那一整批数据既用不上又删不掉内存白白被占着。事后我做了三件事第一全局强制使用统一的 JSON 序列化器不依赖框架默认行为第二所有缓存 Key 加上版本号格式如agent:demo:tool:v2:...以后版本升级直接换 v3不做原地迁移第三加了一个缓存格式巡检脚本每周扫一次所有 Value 的前几个字节如果是二进制乱码类型\xAC\xED开头直接告警。那次之后我再也没有被序列化问题半夜叫醒过。5.4 缓存基础设施最少要做的事Docker 主从与可视化管理Agent 系统对 Redis 的依赖是实打实的单机 Redis 一旦宕机整个 Agent 的记忆和缓存层全挂。哪怕只是开发环境我也建议至少做成主从架构用 Docker 就能快速启动services: redis-master: image: redis:7-alpine container_name: redis-master ports: - 6379:6379 command: [redis-server, --appendonly, yes] redis-replica: image: redis:7-alpine container_name: redis-replica ports: - 6380:6379 command: [redis-server, --slaveof, redis-master, 6379] depends_on: - redis-master主从架构解决的是单点问题主库挂了可以切换从库顶上来。日常运维我还会开一个可视化客户端比如 Redis Desktop Manager用来快速查看 Key 分布、确认某个 Key 的 TTL 和 Value 大小。别小看这个习惯有一次线上用户反馈Agent 突然失忆我用可视化客户端一查发现是某个发布任务把会话记忆 Key 前缀改了新旧 Key 并存导致新的会话读不到旧数据。最后说说我踩过最狠的一个坑有一段时间我把 Redis 当成了无限的垃圾桶什么临时数据都往里塞。直到一次线上 Redis 内存被打满触发OOM command not allowed when used memory maxmemory所有缓存写入直接失败Agent 全线降级为无记忆模式——用户每问一句话都要从头开始。那次之后我给自己定了一条铁律所有写入 Redis 的数据必须同时声明明确的过期时间和淘汰优先级。要么设 TTL要么明确接受 LRU 淘汰绝不允许默认永不过期。Agent 的缓存设计本质上是在成本和体验之间做平衡你想清楚了哪些数据可以丢、哪些数据不能丢Redis 才能从隐忧变成利器。
返回列表