ARTICLE DETAIL

资讯详情

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

缓存治理实战:Redis 成本优化、多级缓存与可观测性

缓存治理实战:Redis 成本优化、多级缓存与可观测性 很多人一提到缓存第一反应永远是“用什么组件”Redis还是本地缓存Spring Cache好不好用缓存集群怎么分片但真正做过几年线上系统的人心里都清楚缓存问题的终点往往不是选型而是成本和可观测性。缓存用得好接口能扛住流量成本还控得住缓存用不好节点加了一堆命中率却低得可怜线上真出问题时连一条有效监控都拉不出来。今天我就把“成本、缓存与可观测性”这三件事放在一起聊重点讲缓存治理、多级缓存、监控埋点以及我踩过的排查实录。文章不算短偏实战适合正在负责系统性能、稳定性和成本的同学慢慢看。1. 缓存治理的本质从命中率到成本模型1.1 为什么缓存治理越来越难做缓存本身不复杂难的是你不知道它到底在系统里扮演什么角色。过去一个后端服务配一个Redis缓存数据也就是用户信息、商品信息、基础配置数量级可控。现在呢本地缓存、分布式缓存、CDN、搜索引擎内部缓存、AI推理场景里的KV Cache还有FPGA加速里的缓存索引全是缓存的变种。它们分散在不同的团队、不同的中间件里业务一扩张几乎没有人能立刻回答三个问题缓存总成本是多少整体命中率长什么样哪些缓存其实压根没必要存在我见过不少团队Redis内存快满了就扩容扩完不到半年又满了。线上QPS上不去第一反应是加节点最后成本翻倍吞吐依然没有改善。说白了大家把缓存当成“存储系统”在管理而不是当成“业务加速层”来治理。存储的目标是尽量多存缓存的目标是做得又快又省。这个思路一转过来你会发现大量内存浪费其实是可避免的。治理难还有另一层原因缓存不直接产生业务可见性。数据库慢查询还能看日志缓存命中不好通常只体现在延迟和资源水位上。等你能感知到异常往往已经要临时扩内存或者被线上故障打脸了。这也是可观测性在缓存治理里格外重要的原因——它是在你和缓存之间建立感知让你提前看见问题而不是等故障发生之后再拿着成本账单事后分析。1.2 从“存得多”到“花得值”缓存成本建模我建议所有准备做缓存治理的团队第一件事是给每个缓存实例建立成本标签。这不是让你马上去买成本管理平台而是先把几个基本字段拉出来归属业务、实例规格、内存水位、每日读写峰值、平均Key大小、命中率、TTL分布、主要存储内容。听起来像个表格活但成本视角一旦建立很多设计缺陷会自己跳出来。举个真实例子。某项目里一块配置数据被全部塞进Redis单条数据只有几百字节总量却上百万条占了好几个GB内存。实际访问频率本身不高1分钟都不到一次。这种场景放在分布式缓存里的性价比较低改成本地缓存或进程内一次加载几十MB就能搞定效果反而更好。不看成本标签你永远发现不了这种无感知的资源浪费。缓存成本可以从几个口径折算内存单价乘以容量、峰值流量乘以外网带宽成本、缓存失效后对数据库回源造成的压力成本。更简单一点可以直接用“成本 缓存资源费用 回源数据库的资源损耗”这个口径来看。缓存并不是越贵越好也不是越便宜越好关键要看它降低了多少下游压力。下面的表格是我常用的一层梳理模板缓存层典型实现主要成本项适合缓存的数据特征本地进程内缓存Caffeine / ConcurrentHashMapJVM堆内存副本数放大成本高频读、变化少、可容忍短暂不一致分布式缓存Redis / KeyDB内存容量、大Key网络开销、主从冗余全局共享、跨实例生效、一致性要求高接入层/边缘缓存CDN / 网关缓存带宽流量、节点缓存空间静态资源、响应体较大但重复读多内容寻址缓存对象存储 本地索引存储容量、索引重建时间大文件、冷数据索引等低频高成本访问这张表的用途是帮你做“值不值”的判断。如果一段数据每分钟被读几十万次多放几层缓存都是划算的如果一天也访问不了几次它压根不应该占用分布式缓存的大块内存。判断标准简单粗暴缓存带来的数据库QPS下降值不值得你付出的缓存内存成本。1.3 实例盘点让缓存成本可见的第一步很多团队连自己有哪些缓存实例都说不全更别提每个实例在哪个集群、哪个账号下、被哪些应用使用。我建议按季度做一次实例盘点不需要很复杂的工具一个Excel就能开始。字段包括实例ID、所属业务、部署Region、规格、分片数、负责人、用途说明、创建时间、最近30天内存峰值、最近30天GET命令量、命中率、TTL策略。盘点过程里你会发现的常见问题包括无人认领的废弃实例、长期低命中的缓存、规格超配但无热点数据的集群。没人认领的废弃实例要尽快销毁低命中但内存大的场景要考虑是否换层次规格超配有大量闲置分片的缓存可以先降配观察指标。这一步做完缓存成本的“账面”才算清晰后面的优化才有基础。2. 缓存选型与多级缓存怎么省钱又不踩坑2.1 先分清不同场景的缓存选型缓存选型不是“Redis打天下”不同数据特征适合放在不同层。比较常见的选择是进程内缓存用Caffeine分布式缓存用Redis或KeyDB边缘缓存用CDN。选择依据主要是三个维度数据共享范围、一致性容忍度、访问频率。需要全局一致的数据比如登录态、库存、风控配置必须放分布式缓存。只在单机内部生效的数据比如计算过程中的中间结果、大列表的内存索引放本地缓存更省钱。对一致性要求不高但希望尽量省流量的静态数据放CDN或网关缓存。选型时要避免一个误区为了“看起来统一”把本地缓存能解决的问题也硬塞进Redis结果网络开销和存储成本都被放大。这里顺便澄清一件事。网上经常有人搜“Spring三级缓存原理”但那套三级缓存指的是Spring容器解决Bean循环依赖时用singletonObjects、earlySingletonObjects、singletonFactories三级缓存来存放实例化过程中的对象跟我们日常聊的多级缓存不是一个概念。Spring三级缓存的核心目的是打破循环依赖而不是给业务数据做加速层。很多面试和团队讨论会把这两个词混在一起容易导致刚接触缓存的人一头雾水。2.2 Spring Cache、Caffeine和Redis怎么组合“多级缓存”最近热度很高尤其搜索词里常出现“Spring Cache多级缓存 Caffeine Redis”。多级缓存的核心价值在于把最频繁的读操作放到离业务代码最近、耗时最低、单价最便宜的存储位置逐级向后淘汰。常见的搭配是Caffeine作为一级本地缓存Redis作为二级分布式缓存。为什么不全部走Redis因为Redis再快也有网络RT和序列化开销。同样的QPS如果全走Redis你可能要扩容到十几个分片本地缓存命中率只要做到50%Redis的压力就能减少一半机器成本直接降下来。为什么不全部用本地缓存因为本地缓存跨实例无法强一致热点数据在每台机器上重复存储也一样费资源并且当服务实例数达到几十上百时局部命中优势会被摊薄。所以规范的组合方式就是两级配合。下面是一个在Spring Boot中的思路示例我会用Spring Cache的抽象来管理但会说明底层逻辑Configuration public class CacheConfig { Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { CaffeineCacheManager localCacheManager new CaffeineCacheManager(); localCacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(5)) .recordStats()); RedisCacheManager redisCacheManager RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30))) .build(); // 这里需要自定义组合CacheManager先查本地再查Redis return new MultiLevelCacheManager(localCacheManager, redisCacheManager); } }实际落地时要注意几个细节。本地缓存和Redis的TTL不要设成一模一样否则同一时刻全部失效会在回源层制造热点。一般我习惯让本地缓存TTL短一些比如3到5分钟Redis长一些比如缓存数据的业务容忍窗口。数据变更时优先失效Redis缓存然后通过发布订阅或者简单HTTP调用让各实例本地缓存也失效如果做不到强一致通知可以用很短的本地TTL兜底。不要迷信“多级缓存一定好”。多级缓存会带来两层缓存失效的复杂性如果业务访问模型本身就是“分散请求低热点”多级缓存的实际收益可能并不明显。做之前最好先看一两天内的缓存命中曲线再决定要不要引入一级本地缓存。2.3 缓存更新与失效写操作的一致性问题缓存里的“写”从来都比“读”麻烦。新手总觉得“读时缓存、写时更新数据库和缓存”最直接但并发一高脏数据几乎必现。典型例子线程A先更新数据库为新值线程B后更新数据库为旧值可B的缓存更新命令晚于A缓存里就停留了旧值。这就是双写不一致。通用且稳妥的策略是Cache-Aside写数据时只更新数据库主动删掉缓存读数据时未命中再回源数据库并写回缓存。用一句话概括就是“缓存不过期就不更新过期了就重建”。这是最符合大多数场景一致性直觉的方案也是我推荐新手优先掌握的缓存更新策略。但Cache-Aside有它的缺陷。删缓存和更新数据库之间有时间窗口可能被并发读请求把旧值重新写进缓存。为了缩短窗口另一个常见方案是延迟双删先删除缓存再更新数据库等待几十毫秒再次删除缓存。背后的逻辑是给最早的并发请求一个“犯错窗口”等它们可能写回旧缓存的时间过去后再做一次清理。我给一个顺序示意删除缓存避免旧缓存继续被读。更新数据库让数据源变成新值。短暂sleep给并发读请求一个很小的重建窗口。再次删除缓存清掉窗口期可能生成的旧值。sleep的具体时间不能拍脑袋要根据业务链路的耗时调整。如果链路很长可以把二次删除改成异步任务比如发MQ或延迟队列。要明确一点延迟双删防的是“并发删除”造成的极端情况但任何缓存方案都无法做到100%实时一致它本质上是拿短暂不一致换取并发下的稳定性。你要有自己的业务容忍边界。2.4 命中率优化与KV缓存原理缓存命中率是所有缓存指标里最直观也最容易骗人的一个。它代表读请求有多少次直接在缓存里拿到了结果。命中高说明缓存起作用了命中低说明大量请求在白白走缓存还是回源了。但实际运营时我要提醒一句话总命中率好看不代表成本合理。在KV缓存模式下命中率优化通常有几个方向。第一尽量避免缓存Key无限膨胀。例如把用户维度数据用固定前缀加ID拆成合理Key不要用一个大Key存储所有用户的JSON字符串那样既浪费内存又容易出现大Key。第二热点数据做单独识别对个别高频Key可以采用本地缓存前置或分片复制分散单分片压力。第三TTL不要设置得过分保守过期时间要加入随机抖动防止大量Key在同一个时刻集体失效。大模型推理场景里的KV Cache命中率也是个典型例子。同一个Prompt前缀如果被多个请求复用推理服务就只需要计算新增的Token部分算力消耗会明显下降这也是现在很多推理服务强调“缓存命中”的原因。类似的逻辑放到业务系统里也一样如果你的查询条件有大量重复前缀比如按天聚合的统计参数那做一层复用缓存会比每次都全量计算划算得多。不过不要只盯着一个全局命中率。我见过团队把命中率当KPI结果命中率99%成本照样失控。原因是命中的全是高频小数据真正产生成本的大Key和低频大Key反而长期未命中。所以更有效的做法是按缓存实例、业务模块、Key前缀三个维度拆开统计定位是哪个维度出了问题。3. 可观测性建设把缓存变成看得见的系统3.1 指标、日志、追踪缺一不可“可观测性”这个词被提了很多次但放到缓存场景里仍然很必要。可观测性不是一个监控面板而是通过指标、日志、追踪三个维度回答三个问题系统什么状态发生了什么一次请求到底经历了什么。缓存的可观测性不是说“Redis有内存监控”就够了。指标层面要包括命中率、过期数量、驱逐数量、请求延迟、命令延迟日志层面要覆盖缓存重建、淘汰、降级、序列化失败等关键动作追踪层面则要能看到一次业务请求里本地缓存、Redis、数据库三层分别花了多少时间。很多团队在缓存故障时只得到一个“Redis很慢”的结论具体慢在哪个命令、哪些Key、哪个分片全都不知道就是因为没有把缓存操作放进请求链路里做追踪。3.2 缓存指标怎么埋最有效下面是我认为缓存可观测性里比较核心的一组指标可以直接参考指标含义参考方向缓存命中率命中的读次数 / 总读次数按实例、前缀、业务拆分禁止只看全局均值缓存回源QPS未命中后打到数据库的请求数与命中率一起看回源高说明热点失效或穿透内存使用率已用内存 / 分配内存超过80%要关注淘汰和扩容节奏驱逐数量缓存因容量不足被淘汰的条目数持续增长说明容量已经不足过期键数量单位时间过期的Key数防止集中过期造成雪崩命令延迟GET/SET的P99/P999拆分到实例级定位慢节点大Key分布超大Key和对应内存占用定期扫描消除隐患以Java项目为例可以使用Micrometer Prometheus的常见组合。如果用了Caffeine开启recordStats()能拿到命中次数、未命中次数、加载成功次数、驱逐数量。如果用了Spring Cache可以在CacheManager上做一层代理每次执行get和put时记录结果和耗时并按CacheName维度汇总。我在项目中习惯给缓存操作做一个AOP切面记录每个Cache的命中率并按5个区间分桶自动标记命中率滑坡的缓存。具体逻辑不算复杂在缓存读取方法上切一下成功返回记命中异常或空值返回记未命中然后把数值累加到Micrometer Counter。这样某类Key的命中率如果从90%突然掉到30%告警平台在几分钟内就能发现不用等人反馈接口变慢。3.3 慢查询、大Key和热点Key怎么追踪可观测性的另一个作用是让“大Key”和“热点Key”及时浮出水面。Redis性能问题里一大半和这两个有关。一个Hash里有几十万个field一次HGETALL就可能把网络带宽打满一个String值几MBGET一次就能让请求延迟飙升。如果不做大Key扫描这类问题会一直潜伏到线上故障。常见排查手段有几种。可以用redis-cli的bigkeys参数做定时扫描生成报告也可以在代理层统计命令长度和响应体大小找出异常流量还可以在业务代码里对读写value的大小做采样统计得到更贴近业务的视角。热点Key的问题就更隐蔽了单个Key流量过高会导致某个分片CPU飙升其他分片却很空闲。要发现热点可以在客户端或代理层记录每个Key的访问频次定期按Key维度排序然后针对热点Key做本地缓存前置或Key打散。我更建议把缓存链路放进追踪系统里。一次请求的Span内部可以记录三个子Span本地缓存读取、Redis读取、数据库查询。接口一旦变慢直接在追踪平台上判断是哪个Span耗时最高。如果Redis读耗时长大概率是大Key或网络抖动如果数据库查询耗时长优先查看缓存穿透和命中率问题。这样形成一套标准化排查路径比每次从头翻日志高效得多。3.4 可观测性工具选型参考工具选择上我建议结合团队现有技术栈不要为了可观测性特意引入全家桶。如果你已经在用Prometheus和Grafana那缓存指标的埋点用Micrometer或Prometheus客户端就好展示层沿用Grafana不需要额外方案。如果需要链路追踪OpenTelemetry是目前生态比较理想的标准可以整合Jaeger、Zipkin或者云厂商Trace服务。日志方面建议统一JSON格式输出方便后续接入ELK或Loki。一个容易被忽视的点是缓存实例本身的可观测性。Redis自身通过INFO命令能看到很多关键指标比如内存、连接数、命令统计、键数量但生产环境通常不建议直接把INFO暴露给外部。更稳妥的方式是通过Redis Exporter这类组件采集prometheus格式的指标再汇入Grafana。缓存集群的慢日志和运行日志也要保留合适的保留周期出问题时能回溯。4. 线上缓存治理实操排查与优化实录4.1 典型问题速查表下面这张表基本覆盖了我实际工作中遇到的高频缓存问题你可以存下来当排查手册用问题表现原因排查方向处理建议缓存穿透DB压力大命中率低查询不存在的Key缓存永远为空看回源QPS、空值分布空值缓存、布隆过滤器缓存击穿热点Key过期瞬间DB被打满高并发热点过期后重建压力集中看热点Key过期时间逻辑过期、互斥锁、本地缓存前置缓存雪崩大面积DB压力暴增大量Key同一时间过期或Redis不可用看过期键数量趋势、Redis故障切换TTL加随机抖动、多级缓存降级缓存与DB不一致查询结果与数据库不一致写更新顺序不对或缓存未删干净打印写操作日志、对比版本号Cache-Aside 延迟双删大Key问题Redis响应慢、带宽打满单个Key存储数据过大扫描大Key、看慢日志拆分Key、压缩、改Hash结构缓存容量突增内存使用率持续上涨Key无上限、TTL过长看Key总数与TTL分布定期清理、容量告警、LRU/LFU淘汰命中率虚高总命中率好看但成本高高频小数据掩盖大Key未命中分前缀、分实例统计按成本维度拆分指标多级缓存不一致本地缓存和Redis值不一致本地缓存未实时失效看本地失效广播日志发布订阅失效、缩短本地TTL4.2 一次真实排查的完整思路分享一个线上案例。某核心接口的P99延迟从40ms涨到200msRedis CPU接近70%但数据库CPU没有明显变化。一开始团队判断是Redis容量不够准备扩内存。我建议先别急着扩容按下面这几步来判断。第一步看命中率。按模块和Key前缀拆分后发现A模块命中率95%B模块只有60%但B模块占用的Redis内存是大头。第二步看Key分布。进一步排查B模块的缓存Key发现它往Redis里写了大量“用户ID时间戳”组合的KeyTTL设了24小时。这种Key在业务上几乎不会复用一天下来几百万个Key全是“写进去就不怎么读”的死数据。第三步看回源QPS。由于大量Key没有命中回源打到数据库。数据库CPU不高但连接线程频繁切换导致接口响应时间升高。第四步修改策略。把这类Key改成只缓存当天活跃用户TTL压缩到1小时同时增加本地缓存做兜底。上线后Redis CPU降到20%接口P99也回落到40ms。这个案例告诉我们一个朴素又重要的道理不要凭感觉去调缓存也不要只看到资源水位就扩容。先看指标再看成本最后动方案。扩容很多时候只是为错误的数据结构在买单。4.3 缓存治理要不要做成平台化如果你的团队缓存实例多、业务线多、稳定性要求高我建议把缓存治理工具化甚至平台化。平台化不是不让业务团队自己用缓存而是提供统一的缓存申请、容量评估、监控告警、巡检报告、成本账单避免各业务线野蛮生长。一个缓存治理平台至少应该包含这些能力实例生命周期管理从申请到销毁都有记录容量和成本预警内存、带宽、连接数超过阈值能自动通知Key规范检查自动识别大Key和长时间未命中缓存变更记录TTL和缓存结构变更有迹可循周期巡检报告自动输出每个实例的命中率、活跃Key、驱逐次数、成本占用。平台化还有个隐性收益把缓存的“踩坑经验”沉淀成自动检查规则。比如发现某个缓存写入了超长TTL但访问量极低系统可以自动标记并给出优化建议某类Key请求量不高但内存占用大系统可以推送告警到负责人。这些规则在平台里沉淀越多后续新人上手越不容易踩重复的坑。4.4 常用工具与收益评估做缓存治理和可观测性并不一定需要很多重型工具。我推荐从最小工具集合起步Prometheus采集指标Grafana做可视化OpenTelemetry做链路追踪Redis Exporter采集Redis指标。如果不想自建也可以直接使用云厂商提供的Redis监控、日志服务和链路追踪产品优先把数据打通再考虑更多定制化能力。落地后的收益评估不要只谈“稳定性”。以缓存治理项目来看你可以把优化前后做对比Redis总内存下降百分比、命中率提升百分比、数据库回源QPS下降、接口P99延迟变化、机器成本变化。这些数据既能为团队带来直观成果也能为下一步扩容或架构调整提供决策依据。没有这些数字支撑缓存优化很容易变成“做了个寂寞”。我个人在实际操作中的体会是成本、缓存与可观测性这三者本质上是一条线可观测性让你看清缓存运行的真实状态成本模型让你判断缓存是否值得缓存设计和治理让性能和成本达到平衡。很多团队只重视性能和功能可观测性却常年缺失最终出了问题只能靠猜测。如果你现在也面临缓存资源浪费、命中率上不去或者排障困难建议先花一周时间把缓存实例全部盘一遍把指标埋好再看哪个环节最疼然后逐个击破。这个顺序一定不会错。
返回列表