资讯动态

缓存架构核心机制与实战:从命中率到一致性设计

发布时间:2026/10/23 5:48:14 来源:尧图企业网站定制
1. 缓存到底在解决什么问题我当年第一次正经接触 Cache是在接手一个慢得让人抓狂的报表系统时。数据库每秒扛着几千次重复查询同样的聚合结果被不同的用户翻来覆去地算CPU 都快烧红了。那时候才真正意识到缓存不是一种“性能优化技巧”而是一种用空间换时间、用分层换效率的基础架构思想。这个章节标题叫“Cache 摘录”其实就是把缓存体系里那些最核心、最容易被忽略、但又决定系统上限的东西集中捋一遍。先给缓存下个最朴素的定义把需要反复读取的数据放到一个比原始存储更快的介质里下次访问时直接从快介质返回省掉重复计算或重复 IO。这句话听起来简单但落到工程上难点几乎全在“放哪里、放多久、失效了怎么办、多个副本怎么保持一致”这些细节上。很多团队一开始只是给数据库加了个 Redis 就以为万事大吉结果遇到缓存穿透、缓存雪崩、缓存与数据库不一致这些问题时才发现缓存的水远比想象中深。在深入拆解之前我建议你先建立起一个分层缓存的全局视图。一般业务系统里会同时存在好几层缓存浏览器本地缓存、CDN 边缘缓存、服务端进程内缓存如 Caffeine、Guava Cache、分布式缓存如 Redis、Memcached、以及数据库自带的 Buffer Pool。每层的访问延迟相差一到两个数量级每一层能承载的数据量和一致性要求也完全不同。搞懂某一层的原理固然重要但真正体现架构能力的是知道一个请求应该优先命中哪一层每一层又该配置多大的容量和多长的过期时间。打个生活化的比方你家里做饭冰箱是“本地缓存”楼下的菜市场是“主数据库”而网上下单次日达的电商是“远端冷存储”。做菜前先开冰箱翻一翻有就直接用没有再去市场买市场也没有那就等快递。冰箱里放什么东西、放多久会坏、买菜回来要不要先塞冰箱这些日常决策就是缓存设计里命中率、过期策略、回源逻辑的缩影。我写这章摘录目标是帮你把缓存这个“工具箱”里的核心工具都认识一遍并且搞清楚每个工具适合什么场景、会踩什么坑。后面所有内容都是我真实项目里验证过的东西不是教科书式的理论堆砌。无论你是后端开发、系统架构师还是正在准备技术面试这章都会让你对缓存有一个系统性的、能落地的理解。2. 缓存的核心机制与关键决策点2.1 命中率缓存价值的唯一度量衡缓存有没有用不看代码写得多漂亮就看一个指标命中率。命中率是指请求在缓存中直接找到结果的占比计算公式很简单命中率 缓存命中的请求数 ÷ 总请求数举个例子一个接口每天被调用 100 万次缓存命中 80 万次那么命中率就是 80%另外 20 万次会穿透到数据库。一次数据库查询假设耗时 50 毫秒缓存查询假设耗时 1 毫秒那么平均响应时间可以这样估算平均响应时间 80% × 1ms 20% × 50ms 0.8ms 10ms 10.8ms如果命中率降到 50%平均响应时间就会飙升到 25.5 毫秒翻了一倍不止。如果命中率是 0那平均响应时间就是 50 毫秒缓存完全白加了。所以做缓存优化的第一件事永远不是调参数而是观察命中率找到导致命中率低的根因。不同场景对命中率的“及格线”也不一样。热点数据集中、读多写少的场景通常能轻松做到 95% 以上而数据量大且访问分散的场景可能 70% 就算不错了。我自己实践中的一个经验值是命中率低于 80% 的缓存方案大概率是设计有问题要么缓存的数据粒度不对要么过期时间设置不合理要么 key 的设计没有考虑局部性原理。提升命中率最有效的手段之一是引入多级缓存。比如先查进程内缓存最快纳秒级没命中再查 Redis毫秒级再没命中才查数据库。每往下一层命中的概率都会增加最终穿透到数据库的流量会被逐层削峰。这里要特别注意的是多级缓存会带来一致性问题——因为同一份数据在多个层里都有副本某层更新了而另一层没更新就会出现“脏读”。后面专门讲一致性的时候我会细说。2.2 过期策略与惰性删除缓存里的数据不能永远不变地躺着。因为底层数据随时可能变化而缓存中的数据只是那个变化在某个时间点的快照。所以每份缓存数据都要有过期时间TTL这是缓存设计中最基础也最容易被设错的参数。TTL 常见的三种设定方式固定 TTL所有缓存数据统一过期时间比如 10 分钟。优点是实现简单缺点是无法适应不同数据的不同变化频率。随机 TTL在基础 TTL 上增加一个随机偏移比如 600 秒正负 60 秒。目的是避免大量 key 在同一时刻集中过期防止瞬时穿透压力波峰。业务自定义 TTL根据数据的实时性要求分别设定比如配置信息缓存 1 小时用户会话缓存 30 分钟库存数据只缓存 5 秒。在 Redis 里过期策略又分两种惰性删除和定期删除。惰性删除是指当某个 key 被访问时才检查它是否过期过期了就删掉定期删除则是后台线程每隔一段时间扫描一批 key删除其中过期的。两者配合使用惰性删除保证了“过期了就不会被读出来”定期删除则保证了“过期的 key 不会长期占着内存”。这个机制我实测下来有个特点如果过期时间设得过长内存中会堆积大量等待定期扫描删除的过期 key内存碎片会变多如果设得过短又会导致频繁回源。所以 TTL 不是拍脑袋定的要结合数据变化频率和访问热度的统计数据来定。另外还有一类“逻辑过期”的方案很多高并发场景会用它规避缓存击穿。做法是缓存里存的不是业务数据而是一个包装对象对象里带着业务数据的值和逻辑过期时间。当请求发现逻辑上已经过期时并不会立即删除缓存而是先返回旧数据同时异步触发一个线程去刷新缓存。这样就把“一个 key 瞬间失效导致大量请求打到数据库”的击穿风险变成了“一个请求触发刷新其他请求继续用旧值”的平滑过渡。代价是极端情况下会短暂读到稍旧的数据换来的却是稳定性和吞吐。2.3 缓存穿透、击穿、雪崩三兄弟这哥仨是缓存面试和实战里出场率最高的话题排着队讲一遍。缓存穿透请求查询的 key 在缓存和数据库中都不存在每一次查询都会直接打到数据库。比如恶意攻击者不断请求一个不存在的商品 ID缓存里没有数据库里也没有所有请求都穿透了。防穿透有三板斧布隆过滤器Bloom Filter在缓存前面挡一道把所有可能存在的 key 在初始化时加入查不到直接返回空空值缓存——把数据库查询结果为空的 key 也写进缓存TTL 设短一点比如 5 分钟防止同一批非法 key 反复穿透参数校验——把明显非法的请求在入口直接拦截掉。布隆过滤器有一个特性它说“不存在”就肯定不存在说“存在”则不一定真的存在有误判率。所以它能拦截掉绝大部分穿透请求但无法 100% 拦截还需要空值缓存兜底。缓存击穿某个热点 key 在缓存过期的那一刻恰好有大量并发请求同时访问它这些请求全部穿透到数据库。和穿透不同的是击穿时数据在数据库中是真实存在的只是缓存刚好过期了。高并发下要防止击穿核心思路是让“重建缓存”这个动作只让一个线程做。实现方式有互斥锁Mutex Key发现缓存没命中时先去抢锁抢到锁的线程查数据库并回填缓存其他线程自旋等待一段时间后再尝试读缓存前面提过的逻辑过期也是一种温和的击穿解决方案还有多级缓存比如热点 key 在进程内缓存里有一份副本即使 Redis 里的 key 过期了进程内的副本仍能扛住一部分请求。实际项目中互斥锁方案最通用逻辑过期方案最适合读多写少且可以容忍数据短暂不一致的场景。缓存雪崩大量 key 在同一时间段集中过期或者缓存节点宕机导致大量请求同时落到数据库。雪崩的杀伤力比击穿更大因为它不是针对某一个 key而是整体性的。预防手段包括随机化 TTL避免同一时刻的过期风暴缓存高可用部署Redis 主从 哨兵或集群模式避免单点故障多级缓存做最后屏障即使分布式缓存全部失效进程内缓存和数据库连接池限流依然能扛住一部分压力接口层做熔断降级当检测到数据库压力超过阈值时直接返回降级数据或友好提示。我的经验是架构设计上要默认“缓存会死”把所有降级路径提前想好而不是等雪崩发生时才手忙脚乱地查原因。3. 缓存淘汰算法到底在淘汰什么3.1 为什么必须淘汰缓存的空间是有限的。一台服务器的内存只有那么大Redis 部署实例的内存也有上限。当新数据要写入缓存而空间已满时就必须按照某种规则选出一部分老数据淘汰掉把位置腾出来。淘汰算法的目标很明确用有限的容量尽可能提高命中率。但这里有个容易混淆的点淘汰策略Eviction Policy和过期策略Expiration Policy是两回事。过期策略是“数据到点了就主动失效”淘汰策略是“缓存满了按规则踢掉一些数据”。两者既独立又互补过期的数据只是逻辑上失效了物理上还占着内存要等删除机制清走淘汰则是物理腾空间的即时手段。Redis 如果设置了maxmemory那么当内存达到上限时就会根据配置的淘汰算法开始干活。3.2 三种经典算法的原理与选型FIFO先进先出谁先来谁先走。这种算法最简单队列即可实现但完全不考虑数据的访问频率很可能把一个高频访问但恰好早进来一步的数据踢掉命中率很难看。现在生产环境用得很少一般只用在规格极小且数据访问无规律的临时场景。LRULeast Recently Used最近最少使用淘汰最长时间没有被访问的数据。它的核心假设是如果一个数据最近被访问过那么未来一段时间内再被访问的概率也比较高。用双向链表 哈希表实现访问一个数据时把它挪到链表头部淘汰时从链表尾部操作时间复杂度是 O(1)。这个算法在绝大多数业务场景下表现良好是默认的选择。LFULeast Frequently Used最不经常使用淘汰被访问次数最少的数据。它的核心假设是访问频率高的数据未来还会频繁访问。如果某个数据在某一小段时间内被狂刷但之后就再也不用了LFU 反而可能让这个历史热点长期占着位置把真正的新热点挤掉。所以 LFU 通常需要引入时间衰减窗口让访问频率随时间推移逐步衰减。选型时的判断依据主要看数据访问的分布形态。以电商场景为例爆款商品的访问频率集中在少数热门商品上且持续稳定LFU 表现更好而资讯类内容热点话题层出不穷、热度快速迁移LRU 更贴合数据访问模式。综合来看大多数通用场景选 LRU明确有长期高频稳定热点的场景选 LFUFIFO 慎用。3.3 Redis 的 maxmemory 策略与配置建议Redis 中实际用的是一种近似 LRU 算法——approx_lru它不维护全局的双向链表而是基于采样来近似判断哪些 key 长时间没被访问。这样做是为了省内存因为真正的 LRU 需要额外的指针维护顺序key 数量很大时这个开销不可忽略。redis.conf中相关配置如下maxmemory 4gb maxmemory-policy allkeys-lrumaxmemory-policy常用的几个值noeviction内存满时不再接受写入直接报错。适合不允许丢失写操作的场景但生产上一般不用。allkeys-lru从所有 key 中按 LRU 淘汰通用性最强。volatile-lru只从设置了过期时间的 key 中按 LRU 淘汰。适合某些需要长期保留、不希望被淘汰的 key 存在的场景。allkeys-random/volatile-random随机淘汰简单粗暴命中率不可控基本只在缓存数据无差别时使用。实际踩过的坑曾经给一个 Redis 实例设了volatile-lru但业务中有一部分 key 根本没设过期时间。结果内存满时那些设置了 TTL 的 key 被大量淘汰引发了一轮命中率骤降和数据库压力飙升。后来改成allkeys-lru把所有 key 一视同仁问题立刻解决。所以配置淘汰策略前先想清楚这个 Redis 实例里是不是所有 key 都允许被淘汰。4. 缓存与数据的一致性怎么保证4.1 先更新数据库还是先删缓存这是缓存领域最经典的灵魂拷问。主流的方案有两个**Cache Aside旁路缓存模式和Write Behind写回**模式。Cache Aside 是现在互联网应用最普遍的一种它的操作序列是读的时候先读缓存不中则读数据库然后回填缓存写的时候先更新数据库再删除缓存而不是更新缓存。为什么写的时候是“删除缓存”而不是“更新缓存”因为更新缓存是一个成本更高且更易出错的方案如果多个线程并发写后写的线程可能把先写的值覆盖了缓存里最终是哪个值完全取决于线程执行顺序而删除缓存则没有这个问题下一次读取时发现没命中自然会把最新数据回填进去。删除比更新更安全。在 Cache Aside 模式下典型的风险窗口是线程 A 更新数据库成功准备删除缓存线程 B 此时读到旧缓存并返回线程 A 删除缓存。这一小段时间内客户端拿到的是旧数据。要彻底根治这个问题最实用的手段是给缓存设置一个较短的 TTL保证即使出现短暂的不一致也会在 TTL 内自愈。极端情况下的最终一致性方案是引入消息队列数据库变更后发一条消息消费者异步删除缓存并做重试机制。4.2 双删策略与延迟双删的细节双删策略是为了解决“先删缓存再更新数据库”方案下的并发问题。它的操作序列是第一次删除缓存。更新数据库。短暂休眠比如几百毫秒。第二次删除缓存。为什么要休眠几百毫秒再删一次因为可能出现这样的情况线程 A 第一次删缓存后线程 B 读缓存没命中于是从数据库读取旧值并回填缓存此时线程 A 更新数据库完成但缓存里已经是线程 B 写入的旧值了。如果不做第二次删除这个旧值就会一直存在缓存里。休眠的作用是等线程 B 的回填动作完成然后第二次删除把它清掉下一次读就会拿到新值。这个方案的问题在于“休眠多久”很难精确控制。如果并发请求量极大回填动作可能在一个较长的时间窗口内反复发生休眠时间不够的话还是会残留脏数据。所以延迟双删不是万能的它降低了不一致的概率但没有从原理上消除。更彻底的做法是订阅数据库的 binlog通过一个独立服务进行缓存删除——数据库一有变更binlog 就产生一条记录消费端拿到后精准删除对应缓存。这样即使业务代码里各种并发顺序乱成一锅粥最终也会被 binlog 驱动的删除动作兜底纠正。这也是很多大型团队采用的“最终一致”方案。4.3 高并发下的强一致诉求怎么取舍需要承认一个现实在分布式系统里缓存和数据库做到强一致代价极高很多时候根本不划算。如果你能接受在极小的时间窗口内读到旧数据那么 TTL 自愈 删除缓存就是性价比最高的方案。如果业务真的对一致性极其敏感——比如库存扣减、订单金额展示——那么正确做法不是花大力气去给缓存做一致性保障而是干脆不要让这些数据进缓存让请求直接打到数据库。缓存解决的是读多写少、一致性要求不高的数据属于强一致范畴的数据不应该为了性能而牺牲正确性。我在项目中见过不少反面案例团队非要给库存数据加缓存为了处理超卖问题引入分布式锁、事务消息、binlog 同步搞了一大堆基础设施最后发现数据库本身的能力完全够用加缓存纯属自找麻烦。所以作为架构师或开发做技术决策时要先问一句这个数据真的有缓存的必要吗命中率能到多少一致性要求是什么回答完这三个问题就知道该不该上缓存了。5. 缓存工具选型与生产实践5.1 本地缓存选型对比本地缓存是指运行在应用进程内部、由应用自身管理的缓存。它最大的优势是快——不经过网络 IO数据就在当前进程的内存里访问时间可以在纳秒级别。代价是缓存数据与应用程序的生命周期绑定应用重启缓存即丢失且多实例部署时每台机器各自维护一份无法做到全局一致。Java 生态中常用的本地缓存框架有 Caffeine、Guava Cache 和 Ehcache。Caffeine 是 Guava Cache 的现代替代品内部实现了基于频率的淘汰算法TinyLFU在读写性能上比 Guava 高一个量级。它的 API 设计得很顺手支持异步加载、基于容量的淘汰、基于时间的过期还能通过recordStats打开命中率统计。一段最简单的用法示例CacheString, Order cache Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(5)) .recordStats() .build(); Order order cache.get(key, k - orderDao.queryById(k));get方法的第二个参数是一个加载函数缓存未命中时自动执行加载逻辑并把结果写入缓存。这个 API 非常适合“读多写少、单次查询成本高”的场景。需要强调一点本地缓存的容量上限一定要根据实际内存情况谨慎设置默认情况下如果配置了很大的maximumSize在流量峰值时可能会占用数百 MB 甚至几 GB 的内存引发 GC 压力甚至 OOM。我习惯在本地缓存之外再配合 JVM 参数-Xmx做总内存预算确保缓存不会把堆挤爆。Go 生态中的本地缓存库有bigcache、gcache、freecache等。其中bigcache的设计思路很特别它用字节数组而不是map来存储极大减少了 GC 压力适合高并发写入、超大缓存量的场景。demo 级代码量很小但每个库的适用场景差异不小选型时重点要看内存分配方式和并发读写模型。5.2 Redis 与 Memcached 怎么选分布式缓存场景下Redis 和 Memcached 曾经是两大主力但现在 Redis 几乎是绝对主流。很多团队从 Memcached 迁到 Redis原因主要有几个Redis 支持丰富的数据结构String、Hash、List、Set、ZSet、BitMap、HyperLogLog不仅是 KV 缓存还能做排行榜、计数器、分布式锁、延迟队列。Redis 支持持久化RDB 和 AOF虽然通常不建议把 Redis 当作可靠存储但有了持久化重启后可以更快地恢复缓存预热降低冷启动对数据库的冲击。Redis 原生支持主从复制、哨兵模式和集群模式高可用方案成熟Memcached 的多节点扩展则主要靠客户端一致性哈希运维复杂度更高。Redis 的SCAN命令可以非阻塞地遍历大 key 集合方便做缓存统计和清理Memcached 在这一点上比较弱。Memcached 也有它的优势纯内存实现、内存分配采用 Slab Allocator 机制内存碎片控制得很好单线程模型的性能在纯 KV 场景下非常稳定且在极大并发下 CPU 开销比 Redis 更低。如果你只需要一个极其简单的 KV 缓存不需要复杂数据结构Memcached 仍然是一个值得考虑的轻量方案。5.3 缓存 Key 的设计规范很多性能问题其实从 Key 设计那一步就埋下了。常见的设计不佳的情况key 过长把整个对象序列化后塞进 key、key 无前缀无命名空间、key 不包含版本号、热点 key 无分片。这些看似细枝末节的东西在千万级 key 的场景下会被无限放大。我在团队里推行的 Key 设计规范大致如下命名结构业务:模块:ID:属性比如order:detail:123456:v1。这样一眼就能看出 key 属于哪个业务域排查问题也方便。控制长度key 越短越好因为 Redis 中 key 本身也占内存。能用数字 ID 就不要拼接长字符串。加版本号如果业务数据的结构发生过升级通过 key 中携带版本号可以平滑迁移否则新旧数据混在一起很难区分。热点 key 分片单个 key 的 QPS 过高时把一个逻辑 key 拆成多个物理 key如hot:1到hot:100读请求先随机选一个分片去读。代价是更新时要把所有分片更新一遍所以在“极高热点 读多极少写”的场景下才值得这么做。我还有一个习惯把所有 key 的生成逻辑收口到一个专门的 KeyBuilder 类或函数里禁止在业务代码里随手拼接 key。这样做的好处是哪天要统一改 key 前缀或者调整命名规则只需要改一处。这个习惯帮我在多次大规模缓存治理中节省了无数赶工时间。6. 高并发场景的缓存实战与排障6.1 缓存预热与冷启动处理系统刚上线、服务重启、缓存集群扩容后缓存里是空的而流量却不会等你慢慢把缓存填满。如果没有任何预热方案冷启动后的第一波流量会全部穿透到数据库轻则响应变慢重则直接打垮数据库。缓存预热的核心思路是在正式流量到达之前把热点数据主动加载到缓存中。一个可落地的预热方案是线上保存最近一段时间内的热点 key 清单可以从日志或监控系统中统计服务启动后异步并发地查询这些 key 对应的数据并回填缓存。比如 Java 中使用线程池并发执行预热任务ListString hotKeys loadHotKeysFromLog(); ExecutorService executor Executors.newFixedThreadPool(16); for (String key : hotKeys) { executor.submit(() - { Object value orderDao.queryById(key); redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10)); }); }预热脚本要控制并发度避免回填请求把数据库冲爆。更好的做法是在流量低峰期预热或者按批次小流量放行。还有一个细节预热时给每个 key 设置一个随机 TTL防止同一时间点整体过期后再次雪崩。如果是 Redis 持久化配置了 RDB 或 AOF那么重启后可以从持久化文件中恢复一部分数据但这取决于最后一条持久化记录的时间点无法保证与故障前状态完全一致。所以不能依赖持久化来做预热还是要有一个外部的、独立于缓存节点的热点清单来源。6.2 缓存监控指标体系怎么搭不监控的缓存等于裸奔。我见过很多系统上线后从不看缓存指标直到某天 MySQL CPU 报警才发现 Redis 命中率已经掉到了 40%。建立缓存监控体系的成本并不高关键是把指标定义清楚。核心监控指标至少要有这么几项命中率hitRate按分钟粒度统计低于阈值时报警。QPS 与延迟缓存读写请求量及 P99 延迟判断缓存是否是瓶颈。内存使用率used_memory / maxmemory接近 90% 时就要关注淘汰率。淘汰 key 数量如果evicted_keys持续增长说明容量不足需要考虑扩容或优化 key 设计。慢查询Redis 中执行时间超过阈值比如 100ms的命令日志通常与 bigkey 操作有关。实现上Spring Boot Actuator Micrometer 可以比较方便地暴露缓存指标到 Prometheus再配合 Grafana 展示。如果用的是 Redis 自带的INFO命令也能拿到最关键的基础指标。我常用的做法是写一个定时任务每 10 秒跑一次INFO stats把keyspace_hits和keyspace_misses两个计数器拉出来算命中率存到监控库里。这样最轻量不需要引入任何额外的探针。注意分析命中率时注意 Redis 的keyspace_hits与keyspace_misses是累计值要先取出差值再计算否则直接把累计值相除会发现命中率越来越失真。6.3 常见故障排查手册故障排查靠的是经验和工具的结合。下面几条是我实际排查中最高频遇到的问题整理成一个速查式的清单问题现象缓存命中率突然骤降排查步骤一确认缓存节点是否发生主从切换新主节点数据是否为空。第二步确认是否有批量任务在近期修改了大量 key 的过期时间。第三步查看监控里淘汰数是否激增判断是容量问题还是 TTL 问题。问题现象Redis 内存暴涨先看INFO memory的used_memory和used_memory_rss是否差距过大如果 RSS 明显大于 used_memory说明有内存碎片率高的问题可能需要执行MEMORY PURGE或考虑重启主节点。再看 key 数量是否暴涨是不是有大 key 写入或者缓存 value 过大。排查命令示例# 查看大 key redis-cli --bigkeys # 查看内存碎片率 redis-cli INFO memory | grep mem_fragmentation_ratio # 查看淘汰情况 redis-cli INFO stats | grep evicted_keys问题现象某个 key 的响应延迟异常高大概率是 bigkey 问题。一个包含几万条数据的 Hash 或 ZSet执行一次HGETALL或ZRANGE很容易达到秒级。解决方法是拆分大 key 为多个小 key或者把大对象拆成多个字段分别缓存。线上排查时--bigkeys是首选工具。问题现象缓存产生脏数据读到的值不是最新值优先检查写路径——是不是用了“更新缓存”而不是“删缓存”再看延迟双删的休眠时间是否足够如果用了消息队列异步删除检查消费者是否积压以及删除失败后有没有重试机制。脏数据问题十有八九是写路径设计的问题不是 Redis 本身的问题。7. 一个完整缓存方案的设计复盘7.1 业务场景与性能指标拿我之前做过的某个商品详情页系统来做个完整的复盘。这个系统的特点是商品数量约 200 万单日详情页请求量约 1 亿次数据库 MySQL 单机 QPS 上限大概 2000 左右商品信息变更频率中等编辑一次后短时间内不会频繁变动用户对数据的强一致要求不高要求展示结果在 30 秒内追上最新变更即可。初期方案没上缓存时数据库 QPS 峰值达到 5000 以上MySQL 的 CPU 长期 90% 以上慢查询日志刷屏。团队当时加了一个 Redis 缓存缓存了完整的商品详情 JSONTTL 设为 24 小时结果发现商品价格变了之后用户端最迟要一天才能看到新价运营那边炸锅了。这是一个典型的不理解“缓存一致性”而导致的失败案例。7.2 目标方案设计重新设计后的方案如下第一层CDN 缓存静态资源html、css、js、图片都由 CDN 加速这一层几乎不占用后端资源。第二层服务端本地缓存Caffeine缓存商品基础信息容量 10 万条TTL 60 秒。为什么放一层本地缓存因为商品详情页的 QPS 高达几千到几万热点商品的访问集中度非常高本地缓存能扛掉很大一部分请求降低 Redis 压力。第三层Redis 缓存缓存商品详情的完整聚合数据TTL 15 分钟使用allkeys-lru淘汰策略。这里把 TTL 从 24 小时改成 15 分钟是为了让缓存数据在可接受的延迟内自愈舍掉了一部分“绝对命中率”换来了远端缓存与数据库的一致性。第四层MySQL 数据库承接所有缓存未命中的查询。写路径运营后台编辑商品后先更新 MySQL然后通过 binlog 订阅服务异步删除 Redis 缓存。由于本地缓存 TTL 只有 60 秒即使本地缓存短时间内未失效也能在 1 分钟内追上最新数据。这套方案上线后数据库 QPS 从 5000 降到 300 左右接口 P99 延迟从 400ms 降到 45ms命中率整体在 92% 以上运营的修改在 1 分钟内全部生效。整体思路其实并不复杂但每一步的 TTL、淘汰策略、一致性方案都是根据真实访问数据调出来的而不是套模板。7.3 容量与成本估算设计任何缓存方案时都要先拍一下容量账。商品详情 JSON 平均大小约 5KBRedis 中缓存 15 万个热点商品覆盖 90% 以上的请求需要的空间是150,000 × 5KB × 1.5Redis 内部对象开销和碎片率 ≈ 1.1GB再加上 20% 的预留空间Redis 实例设置maxmemory 1.5gb足够。本地缓存 10 万条每个对象平均 3KB加上 Caffeine 内部的结构开销堆内占用大约 500MB需要给 JVM 多留出这部分堆内存预算。记得把本地缓存从堆内存中隔离计算或者用堆外缓存方案避免影响正常业务对象的 GC 表现。这些计算看起来很简单但在真实项目里很多团队就是没提前算等到内存溢出了才开始逐个排查是哪份缓存吃掉了内存。提前把容量账算清楚能省掉后面很多运维救火的痛苦。8. 我对缓存设计的一些总结与心得回到章节标题“Cache 摘录”我写的这些不是缓存技术的全部但它们是缓存工程实践中最常被用到、也最容易被搞砸的部分。缓存不是一个可以“加了就完事”的组件它是一整套需要持续关注命中率、延迟、内存、一致性、故障恢复的生命周期管理体系。几个我反复验证过的心得最后再强调一遍第一个缓存设计从“数据该不该缓存”开始而不是从“用 Redis 还是 Caffeine”开始。考虑缓存之前先想清楚问题是什么是数据库压力大还是接口响应慢如果是数据计算链路过长导致的慢也许应该优化计算逻辑而不是加缓存。第二个一切缓存参数都要用数据驱动。TTL 设多长、容量开多大、淘汰算法选哪个全部要有监控数据支撑。不要凭感觉拍脑袋配置然后出了问题再反复调。第三个缓存是系统的加速器不是系统的存储层。任何放进缓存的数据都要做好“随时消失也影响不大”的心理准备。如果一份缓存数据的丢失会导致系统功能不可用那这份数据本来就不该只放在缓存里。第四个技术方案越复杂越容易在生产环境翻车。延迟双删、binlog 订阅、分布式锁、多级缓存这些都是解决特定问题的工具不是必须全部上的配置。如果一个简单的 TTL 自愈方案能满足业务需求就不要为了让架构“看起来高端”而引入额外的复杂度。我见过太多过度设计的缓存方案最后反而是最简单的方案跑得最稳。另外分享一个小技巧排查缓存问题时先看监控数据再动手改代码。很多缓存问题的表象在应用层根因却在容量规划或 key 设计上。比如接口突然变慢新手第一反应是去优化代码老手则会先看一眼命中率和 Redis 慢查询很多时候问题就已经水落石出了。这个习惯比记住任何具体技术点都值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑