缓存该删还是该改,LLM 服务里的双写问题

缓存该删还是该改,LLM 服务里的双写问题

给答案加缓存之后,命中率上去了,账单下来了。然后开始出现一类很难查的问题,同一个问题,五分钟后问一次得到新答案,再问一次又回到旧答案。用户以为系统在抽风。

这类问题的根源在双写。数据变了,数据库和缓存都要动,两个地方不是原子的,中间那段窗口就会出不一致。而缓存这种不一致一旦形成,可能一直存在,直到过期。

为什么推荐删而不是改

两条路都能走。数据变了,把新值写进缓存,或者把缓存删掉等下次读时重建。

推荐删的理由有三个,每一个都跟并发有关。

第一个,改缓存有顺序问题。两个请求同时更新同一条数据,A 先拿到旧值改成新值一,B 后拿到旧值改成新值二,如果 B 的缓存写入先到、A 的后到,缓存里留下的是新值一,数据库里是新值二。这个不一致会一直留着,直到过期。删缓存没这个问题,因为删除是幂等的,谁先删谁后删结果一样。

第二个,改缓存需要读本来不该读的数据。很多更新操作只知道哪几个字段变了,为了写缓存要先把整条读完,多一次读,多一个失败点。

第三个,删缓存的成本低。删一个键是常数开销,写一个可能很大的对象不是。

但删也有窗口

删缓存的经典问题是这个顺序,先删缓存、再写库。如果中间有一个读请求进来,它读到旧库值,把它加载进缓存,然后写库完成。缓存里留下了旧值,而且没有过期之前一直错。

所以常见做法是先写库、再删缓存。这样窗口小很多,因为写库之后立刻删,读请求撞上窗口的概率低。但不是零,如果读请求在写库之前读到旧值,在删除之后才写进缓存,还是错的。

降低这个概率的手段是延迟双删。写完库之后删一次,过一小段时间再删一次,把那个抢跑进来的读请求写下的脏值清掉。延迟多久不好定,通常给几百毫秒。

版本号是最稳的兜底

上面所有手段都是降低概率,没有一个能给保证。想要保证,得让缓存自己知道它是不是过期数据。

做法是给数据带版本。缓存里除了值,还要存这个值的版本号或者更新时间。读的时候,用缓存里的版本和数据库里的当前版本比一下,不一致就丢弃缓存重新加载。

这样即使脏值被写进缓存,下一次读也能识别出来。代价是每次读要多查一次版本,通常查的是一个很小的字段,或者单独一个版本表。

在 LLM 服务里,这个方案特别适用,因为要缓存的东西往往本来就有版本概念。提示词有模板版本,知识库有文档版本,答案缓存可以带上游数据的最大更新时间。

三种不一致要分开看

第一种是写后读。用户刚改完,立刻刷新,看到的还是旧值。这是最容易被投诉的,因为用户能感知。

第二种是并发写。就是前面说的顺序颠倒。这种不一致持续时间最长,也最难查,因为它是随机的,重试一次可能就对了。

第三种是主从延迟。数据库有从库,写完主库立刻读从库读到旧值。这一种和缓存无关,但现象一样,排查时容易混淆。

排查时先确定是哪一种,看时间戳和请求日志的先后。混在一起猜,一定猜错。

缓存空值也有坑

查不到的数据要不要缓存空值。缓存的好处是防止穿透,坏处是数据后来补上了,缓存里还是空。

解法是空值给一个很短的过期时间,比如三十秒,长数据给正常过期时间。这样既挡住了穿透,又不会长期错。

还有一类特殊情况,负缓存和正缓存用同一个键空间。数据从无到有的时候,要显式删掉那个空值标记,否则用户会在一段时间内看到查无此数据。

LLM 服务里哪些缓存不用删

不是所有缓存都要走这套逻辑。

按内容哈希缓存的嵌入向量,天然不需要删。同样的文本永远得到同样的向量,除非换模型。换模型时整批失效,按模型版本换前缀就行。

提示词模板的渲染结果也可以按内容哈希缓存,因为输入相同输出必然相同。

只有那些依赖外部状态变化的缓存,才需要这套删除加版本的处理。答案缓存、业务数据摘要、工具调用结果,都属于这一类。

分清这两类很重要,不然你会为了不需要一致性的东西写一堆逻辑。

更新频率低的数据别折腾

有些数据一天改一次,缓存时间本来就短。这种场景上复杂的双写方案,收益很低,风险反而高,因为逻辑复杂了自己会出 bug。

判断标准很简单,如果脏数据的最大持续时间在你的容忍范围内,就别做。

容忍范围怎么定,问业务。价格错五分钟能接受吗,库存错五分钟能接受吗,个人资料错五分钟呢。答案不一样,方案也不一样。

结合系列

Redis 那篇讲的是这套缓存的物理形态,键怎么设计、过期怎么定、大键怎么拆。缓存那篇讲的是命中率和成本,这一篇补上一致性这一维。配额那篇的计数缓存也需要一致性,但它的容忍度高得多,因为计数本来就是近似值。数据脱敏篇的缓存要额外注意,删缓存时要确保受限数据不会残留在别的键里。可观测性篇的 trace 要记录缓存命中与否,否则你分不清是没命中还是命中了脏数据。

相关推荐
余槐i1 小时前
Spring Boot 3.2 偶发500无日志:线程池复用导致请求上下文丢失
spring boot·redis·后端·mysql·性能优化
IT规划师2 小时前
Redis 数据类型全解 - ZSet:两个编码,七种玩法(五)
redis·.net
我不会起名字32211 小时前
Redis 从 0 到 1(十七):订单 30 分钟不支付自动关闭——ZSet 延迟队列 Go 实现
redis·go·延迟队列·zset·订单
利威尔xu12 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
利威尔xu13 小时前
Go IM 源码实战(13):30 个会话只有 2 个红点,未读数错在三条路径
redis·后端·微服务·面试·golang
will_net15 小时前
Redis集群部署+Redis 链接地址放入Aollo +c#项目接入Redis
redis·c#·bootstrap
合天网安实验室16 小时前
从压缩到泄露:KV Cache Compression 的选择性失忆与系统提示词风险
人工智能·llm
用户69190268133917 小时前
Redis 1:简述Redis概念与Agent的联系
redis·后端·设计