Caffeine本地缓存实战:从Window-TinyLFU到多级缓存一致性

发布时间:2026/10/10 20:13:33
Caffeine本地缓存实战:从Window-TinyLFU到多级缓存一致性 做Java后端时间久了你会发现“缓存”这个词被聊烂了。Redis、Memcached这些分布式缓存扛下了大多数压力但还有一个容易被低估的选项——进程内本地缓存。前几年我陆续把项目里的Guava Cache替换成Caffeine线上接口的P99延迟降了一截GC频率也明显改善。Caffeine就是那个常被叫做“Java缓存之王”的本地缓存库基于Java 8开发性能和命中率都压过老牌Guava Cache一头。这篇文章不打算读者已经把玩过Caffeine而是从“为什么该用它”讲到“怎么用好它”再落到多级缓存、数据一致性这些实战问题。无论你是刚接触Java缓存的新手还是准备面试需要系统梳理Caffeine相关知识点的人或者已经在生产环境用了、想排查一些坑的老手这篇都能给你一些参考。我会把算法原理、核心API、配置参数、Spring Boot集成、常见故障这些内容全部揉在一起按实际操作的经验来讲顺手补充一些普通文档里不会写的细节。1. 为什么是Caffeine从Guava到Caffeine的升级逻辑1.1 别再拿ConcurrentHashMap当缓存用了很多项目一开始偷懒直接用ConcurrentHashMap做缓存。key放请求参数value放查询结果看起来简单高效。但用一段时间就会出问题它没有容量上限没有过期机制也没有淘汰策略。流量一涨Map里的对象只增不减最终老年代被打满GC时间飙升甚至直接OOM。相比ConcurrentHashMap这样“裸”的容器Caffeine这类缓存库解决的不只是“存和取”还包括三个核心问题容量控制超过maximumSize之后能自动驱逐防止内存无限增长。过期机制支持写入后过期、访问后过期和自定义过期时间。淘汰策略用高效算法决定“哪些数据该留下哪些该让位”追求高命中率。ConcurrentHashMap、Guava Cache、Caffeine三者的定位差异下面这张表能看得很清楚维度ConcurrentHashMapGuava CacheCaffeine过期时间不支持支持支持容量淘汰不支持支持支持淘汰算法无LRU近似Window-TinyLFU异步加载不支持不支持支持操作统计不支持简单完整命中率统计读性能高中很高学习成本低中中结论很直接只要你的本地缓存有容量或过期需求就不要自己用Map硬扛。Caffeine应该成为Java本地缓存的首选而不是备选。1.2 性能对比Caffeine凭什么说自己是“缓存之王”Caffeine敢自称“缓存之王”主要靠两点一是基于论文《TinyLFU: A Highly Efficient Cache Admission Policy》实现的Window-TinyLFU淘汰算法二是针对高并发读场景做的无锁优化。在官方给出的Benchmark里Caffeine的读吞吐量比Guava Cache有明显优势部分场景接近翻倍甚至更高。这背后的原因不是Caffeine用了什么魔法而是它的设计目标就是“高并发读多写少”的缓存典型场景读操作大多数走无锁路径少量写操作使用分段机制降低竞争避免全局锁成为瓶颈。我自己在项目里做过一个小压测同一个接口用Guava Cache和Caffeine分别做本地缓存QPS拉到5000以上时Guava的CPU占用明显比Caffeine高出一截原因是Guava的LRU淘汰需要维护访问顺序结构在高并发下竞争更激烈。Caffeine则用近似算法换取了并发性能命中率反而没有下降太多这笔账怎么算都划算。注意这里说的都是“进程内缓存”也就是每个JVM实例自己维护一份。要解决多实例共享、数据全局一致的问题还得靠Redis这类外部缓存。本地缓存和分布式缓存不是替代关系而是互补关系。1.3 选型思考什么时候该上Caffeine不是所有项目都需要Caffeine。如果你的接口几乎没有热点数据或者每次查询都强依赖最新数据本地缓存反而会成为负担。适合上Caffeine的典型场景读多写少的热点数据比如配置信息、字典表、商品详情、用户基本信息。允许短暂不一致比如运营配置容忍几秒到几分钟的延迟同步。单机吞吐要求高比如秒杀场景下的库存预校验、风控规则匹配。距离数据库远、查询成本高避免每次请求都打DB或打远程服务。不适合的场景强一致要求数据更新后必须立刻可见不能接受任何延迟。写极多每次写入都要同步更新缓存缓存反而放大写放大。单条数据极大、总量极大本地缓存会占满堆内存需要评估是否分片或换外部缓存。我的经验是先问自己“这个数据能不能容忍几秒的不一致”能容忍再考虑Caffeine不能就直接查DB或用Redis外加分布式锁。2. Caffeine核心机制Window-TinyLFU淘汰算法拆解2.1 从LRU和LFU的缺陷说起要理解Caffeine的高命中率先得理解传统淘汰算法为什么不够好。LRULeast Recently Used淘汰最久没被访问的数据。问题在于一次性的批量扫描会把热点数据全部挤出缓存。比如一个冷门接口突然被脚本遍历大量IDLRU会把这些一次性数据全部装进缓存把真正的热数据挤掉等热数据下次访问时又要重新加载。LFULeast Frequently Used淘汰访问频率最低的数据。问题在于频率计数需要额外存储而且一个昨天特别热、今天已经没人访问的老数据因为历史计数高会一直占着位置不走新热点反而进不来。简单的LRU和LFU放到真实业务里都容易翻车。Caffeine的做法是把两者结合起来再引入频率近似估计和衰减机制这就是TinyLFU的大致方向。2.2 TinyLFU用近似计数换空间TinyLFU的核心思路是用更少的内存记录每个key的访问频率并且在合适的时候对频率做“减半”处理防止旧热点霸占位置。传统LFU为每个key维护一个精确计数器内存开销高。TinyLFU换了一种思路使用布隆过滤器类似的概率数据结构Count-Min Sketch来记录访问频率。它不是为每个key单独存一个计数而是用多个哈希函数映射到一组计数器数组上读取频率时取多个位置的最小值来近似估计。这样做的效果是空间占用极小一份频率记录表只需要很小的内存就能覆盖海量key。会有一定的计数误差多个key可能哈希到同一个计数器位置导致频率被略微高估。但缓存淘汰本身不要求精确只要“大致区分热与冷”就够了。在数据规模较大时为了防止旧数据频率累积过高、永远无法被淘汰TinyLFU有一个关键机制——频率衰减。当整个频率表的计数总和超过一定阈值所有计数值统一减半。这个过程叫做reset所以新数据才有机会崭露头角。2.3 Window Cache给新数据一个“试用期”如果只用TinyLFU又会遇到一个问题新进入缓存的数据频率计数都是1很难竞争过已经积累了频率的老数据结果就是新数据几乎无法存活。这不符合缓存的实际需求——新数据常常就是未来一段时间的热点。Caffeine因此增加了一个Window Cache区。整体结构分成两部分Window区容量只占整个缓存的1%采用LRU策略。Main区占99%其中又分为Protected区和Probation区。数据刚进入缓存时先进Window区。在Window区里新数据完全按照LRU逻辑运行有一个“出生保护期”。如果在这个窗口期内被再次访问它就有机会进入Main区如果一直没被访问就会被淘汰。这段话翻译成大白话就是Caffeine给了新数据一个“试用期”试用期内表现好就转正表现不好直接走人。这样一来既保留了对突发热点的高响应速度又通过频率记录保住了高频老数据两种优势都吃到了。2.4 候选人淘汰一场靠频率决胜负的比试Main区内部的运作逻辑也很有意思。我把整个淘汰过程拆开看Window区满了最旧的候选者被挤出来。这个候选者默认进入Main区的Probation区待观察区。如果Probation区也满了新的候选者就要和Probation区里最旧的“住户”比试排序。比试时Caffeine用Count-Min Sketch查询双方的历史频率。如果新来的频率更高就淘汰旧的让新数据转正如果旧住户频率更高新来的就出局。Protected区是Main区里频率较高、表现较好的数据它们被驱逐的概率很低。只有当Protected区满了又面临新数据晋升压力时才会被降到Probation区。这个设计非常像公司里的晋升机制新人有试用期Window试用期过了进入预备池Probation表现好晋升为核心员工Protected表现下滑被下放到预备池最终面临被新人顶替的命运。Caffeine的命中率高并不是靠某一个单一策略而是靠这套分区分工和频率竞争机制共同实现的。理解了这个模型后面配置参数时就不会瞎填了。3. 上手实操Caffeine的4种加载方式与核心API3.1 Maven依赖与基础配置引入Caffeine非常简单Maven依赖如下dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId version3.1.8/version /dependency注意Caffeine 3.x要求Java 8及以上。如果是Java 11或17环境用3.x完全没问题。如果你的项目还在Java 7只能用2.x老版本功能会有差异。构建一个最基础的缓存只需要一行CacheString, Object cache Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(5)) .build();这个缓存最多容纳1万个key每个key写入5分钟后过期。先记住这个最小配置后面再逐步加复杂度。3.2 手工加载模式Manual Cache最灵活手工加载模式下缓存本身不关心数据从哪里来你得自己写加载逻辑。核心方法是get(key, mappingFunction)这个方法会在缓存未命中时执行mappingFunction加载数据并把结果写入缓存。CacheString, UserProfile profileCache Caffeine.newBuilder() .maximumSize(5_000) .expireAfterWrite(Duration.ofMinutes(10)) .build(); UserProfile profile profileCache.get(userId, id - userService.loadProfile(id));如果userService.loadProfile返回nullCaffeine不会缓存null值下一次访问还会重新执行加载逻辑。这跟“null值会导致缓存穿透”是一个道理。如果你确实要缓存空值可以包装一个Optional或者用特殊标记对象占位。手工加载模式适合缓存逻辑比较复杂、每次加载前需要额外判断的场景。比如查询用户时先看状态位状态异常就不缓存比如加载时需要传入多个参数需要把参数封装成一个key。3.3 同步加载模式Loading Cache省心省事同步加载模式最直观构建时传入一个CacheLoader之后调get(key)时如果缓存未命中Caffeine会自动调用loader方法加载数据LoadingCacheString, UserProfile profileCache Caffeine.newBuilder() .maximumSize(5_000) .expireAfterWrite(Duration.ofMinutes(10)) .build(key - userService.loadProfile(key)); UserProfile profile profileCache.get(userId);需要注意LoadingCache的get方法是同步的。如果加载数据很慢比如远程调用要几百毫秒那么高并发下同一时刻大量未命中的请求会同时触发loader方法可能把下游服务打爆。好在Caffeine内置了自动去重机制同一时刻多个线程请求同一个key时只会有一个线程真正执行loader其他线程阻塞等待结果。这个机制叫“single-flight”可以有效防止缓存击穿。不过要注意如果loader方法抛异常其他等待线程也会跟着收到异常。3.4 异步加载模式Async Cache不阻塞主线程如果加载成本很高且你不想让调用线程白白阻塞等待可以试试异步加载AsyncCacheString, UserProfile asyncCache Caffeine.newBuilder() .maximumSize(5_000) .expireAfterWrite(Duration.ofMinutes(10)) .buildAsync(); CompletableFutureUserProfile future asyncCache.get(userId, id - userService.loadProfileAsync(id));注意看这里的差异get方法返回的是CompletableFuture。如果你的数据源本身就是异步接口异步加载模式可以天然适配避免把异步能力“抹平”成同步等待。异步模式下同样支持异步LoadingCacheAsyncLoadingCacheString, UserProfile asyncLoadingCache Caffeine.newBuilder() .maximumSize(5_000) .expireAfterWrite(Duration.ofMinutes(10)) .buildAsync(key - userService.loadProfileAsync(key)); CompletableFutureUserProfile future asyncLoadingCache.get(userId);异步缓存有个容易踩的坑如果任务真正执行时抛了异常这个异常会被封装到CompletableFuture里而不是直接抛给调用线程。如果你没调用future.get()或没加exceptionally处理异常会被静默吞掉影响排查。所以异步缓存最好配合全局异常处理逻辑一起使用。3.5 四种加载方式对比模式适用场景调用方式加载异常处理Cache手动自定义逻辑复杂get(key, fun)直接抛出可自行捕获LoadingCache同步数据源为同步方法get(key)直接抛出自动single-flightAsyncCache异步数据源为异步方法get返回Future封装到Future中需显式处理AsyncLoadingCache异步自动加载get返回Future同上我的建议是如果数据源是同步阻塞调用优先用LoadingCache如果是异步接口优先用AsyncCache。手动模式留给那些有特殊逻辑的场景不要一开始就用它否则你会发现到处都在写加载函数代码看起来反而更乱。3.6 驱逐策略配置详解大小、时间、引用Caffeine的配置项看着没几个每一个背后都有讲究。按大小驱逐CacheString, Object cache Caffeine.newBuilder() .maximumSize(10_000) .build();按key的数量驱逐最简单的类型。适合缓存对象大小相对均匀的场景。如果缓存对象大小差异很大比如有的value只有几十字节有的value有几十MB建议用maximumWeight按权重驱逐CacheString, Object cache Caffeine.newBuilder() .maximumWeight(100 * 1024 * 1024) .weigher((key, value) - computeValueSize(value)) // 返回权重值单位自定 .build();设置maximumWeight时必须设置weigher否则构建会直接报错。权重值可以自己定义字节数、条数都行但要保证同一缓存内权重口径一致。按时间驱逐// 写入后多久过期最常见 .expireAfterWrite(Duration.ofMinutes(10)) // 最后访问后多久过期 .expireAfterAccess(Duration.ofMinutes(10)) // 完全自定义过期时间灵活但复杂 .expireAfter(new ExpiryObject, Object() { Override public long expireAfterCreate(Object key, Object value, long currentTime) { return TimeUnit.MINUTES.toNanos(10); } Override public long expireAfterUpdate(Object key, Object value, long currentTime, long currentDuration) { return currentDuration; } Override public long expireAfterRead(Object key, Object value, long currentTime, long currentDuration) { return currentDuration; } });这里必须提醒一个容易被忽略的坑expireAfterAccess是“每次访问都重新计时”也就是说一个key只要持续被访问就永远不会过期。如果你拿它做用户会话缓存活跃用户的token会一直活着内存压力会越来越大而且违背了“会话登录态应该定期失效”的初衷。按引用驱逐Caffeine还支持弱引用和软引用weakKeys()key使用WeakReferencekey对象没有被其他地方强引用时可能被GC回收。weakValues()value使用WeakReference适合缓存的对象生命周期由外部控制。softValues()value使用SoftReference当内存紧张时GC会回收这些软引用对象适合缓存体积较大的对象。引用类型驱逐看着美好但有一个严重问题它依赖GC动作执行导致缓存中数据的消失时间不可控。生产环境我很少建议大家用softValues因为“内存不足时再回收”对缓存来说也太晚了而且缓存存在这个对象又希望它被GC回收这个逻辑本身就有些拧巴。我的实践结论设计缓存时先按maximumSize控数量按expireAfterWrite控时间这两个是最稳的组合。特殊场景再考虑weight和引用类型不要一上来就堆满所有配置。4. Spring Boot集成与多级缓存实战4.1 Spring Cache Caffeine注解式缓存Spring Cache通过Cacheable注解让缓存接入变得很优雅。先把CaffeineCacheManager配置好Configuration public class CacheConfig { Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager new CaffeineCacheManager(); // 缓存名称统一前缀避免不同业务缓存相互干扰 cacheManager.setCacheNames(Arrays.asList(user, product)); cacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(10)) .recordStats()); return cacheManager; } }业务代码里用注解即可Cacheable(cacheNames user, key #userId) public UserProfile getUserProfile(String userId) { return userService.loadProfile(userId); }几点经验cacheNames不要只设置一个通用名字最好按业务域拆分方便后续调整每个缓存的容量和时间。Cacheable默认key是SpEL表达式如果方法参数是一个对象最好显式指定key否则整个对象作为key会浪费内存。Spring Cache只是“门面”底层仍然走Caffeine的API那些性能和算法优势依然保留。4.2 多级缓存本地Caffeine Redis DB单机本地缓存终究解决不了分布式环境下的所有问题。生产环境最常见的搭配是Caffeine做本地一级缓存Redis做二级缓存数据库兜底。访问流程如下请求进来先查Caffeine命中直接返回。Caffeine未命中查Redis命中则回填Caffeine并返回。Redis未命中查数据库结果同时回填Redis和Caffeine。这个流程每多一层就多一次网络IO或内存查询但命中率也更高。Redis虽然速度快但一次网络往返在0.1ms到1ms之间相比本地内存的几十纳秒差距还是很大的。所以热点数据如果能落在Caffeine里高并发场景下的响应时间会好看很多。代码实现上典型的多级缓存工具类大致长这样public class TwoLevelCache { private final CacheString, Object localCache; private final StringRedisTemplate redisTemplate; public Object get(String key) { Object value localCache.getIfPresent(key); if (value ! null) { return value; } String json redisTemplate.opsForValue().get(key); if (json ! null) { Object obj JSON.parse(json); localCache.put(key, obj); return obj; } Object dbResult loadFromDb(key); // 实际应使用函数式方法避免耦合 redisTemplate.opsForValue().set(key, JSON.toJSONString(dbResult), Duration.ofMinutes(30)); localCache.put(key, dbResult); return dbResult; } }4.3 数据一致性缓存更新的老生常谈多级缓存真正的难点不是读而是写。数据库更新了缓存还是旧值用户看到的就是脏数据。我这里分享几种实践中常用的方案。方案一先更新数据库再删除缓存这是最简单也最常用的一致性方案。更新DB成功之后把Caffeine和Redis里的key删掉让下一次请求触发重新加载。整个过程只需要一行invalidate操作// 更新数据库 userService.updateProfile(userId, newProfile); // 删除两级缓存 localCache.invalidate(userId); redisTemplate.delete(user: userId);这种方式在大多数业务场景下够用。唯一要注意的是删除缓存之后、请求重新加载之前如果刚好有并发读就会把旧值重新写回缓存。处理办法是延迟双删先删除缓存再更新DB等几百毫秒后再次删除缓存。但这会把简单方案变复杂实际业务如果脏数据窗口可以接受直接用简单方案就好。方案二消息队列通知失效在分布式部署下每个JVM实例都维护了各自的Caffeine。更新DB的请求可能落在A实例上A实例删了自己的本地缓存但B、C实例的本地缓存还是旧值。解决这个问题需要引入消息通知更新DB。发送一条“缓存失效”消息到MQ。所有应用实例监听消息收到后删除各自的本地缓存和Redis缓存。这种方式能够保证最终一致但引入了额外依赖。如果你的系统架构简单实例数不多或者业务上对短暂不一致容忍度高可以退一步只删Redis 缩短本地缓存过期时间。我的多级缓存一致性实践原则本地缓存过期时间越短越好通常5分钟以内超过10分钟就要认真评估脏数据窗口。更新操作必须主动失效不能只依赖过期时间。用MQ广播失效消息时消息必须包含业务key监听到后invalidate Redis删除双管齐下。对极端一致性敏感的数据比如余额、库存不要走缓存直接穿透查询或加分布式锁。方案三版本号机制给每一条缓存加一个版本号或更新时间字段。读取时把版本号一起返回给前端前端请求时带上版本号后端发现版本号不是最新就强制刷新缓存。这个方案适合接口层做数据新鲜度校验但需要前端配合不是所有场景都适用。4.4 监控与统计别让缓存变成黑盒上线缓存之后最怕的问题就是到底命中率高不高缓存有没有生效内存占用多少Caffeine自带统计功能开启方式很简单CacheString, Object cache Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build();之后通过cache.stats()可以获取hitCount命中次数missCount未命中次数hitRate命中率loadSuccessCount、loadFailureCount加载成功/失败次数evictionCount驱逐次数evictionWeight驱逐的总权重生产环境可以把这些指标接入Prometheus或Spring Boot Actuator做成Grafana面板。一个健康缓存应该满足以下特征高并发接口的命中率应保持90%以上。驱逐次数如果持续增长说明缓存容量已经不够了。加载失败次数如果频繁出现说明下游数据源不稳定需要重点排查。以下是统计对象设置的注意事项recordStats()开启会额外占用一点点CPU和内存如果缓存QPS非常高、且你对命中率变化完全不关心可以不开。5. 常见问题与排查技巧实录5.1 常见问题速查表用表格把实际遇到最多的问题整理出来方便读者直接对照排查。问题现象可能原因解决办法缓存命中率极低key设计不合理每次请求都生成新key检查key的拼接规则收敛key粒度缓存内存暴涨没有设置maximumSize设置容量上限压测后合理调整数据更新后还是旧值主动失效没做或只删了本实例本地缓存增加更新后的invalidate操作消息通知所有实例缓存永不失效expireAfterAccess 高访问频率改用expireAfterWrite或组合使用过期策略高并发下downstream被打爆未命中时大量请求同时执行加载逻辑使用LoadingCache或加single-flight机制异步缓存异常无日志异常包装在Future中未处理添加whenComplete或exceptionally处理缓存对象太大导致GC压力value体积大 缓存容量大限制单个对象大小或改用最大权重驱逐5.2 面试视角Caffeine相关问题怎么答很多人在面试时被问到“Caffeine和Redis有什么区别”这类问题回答时容易陷进“一个本地一个分布式”的浅层对比里。我建议从三个层次组织答案既体现广度又体现深度。第一层定位差异。Caffeine是进程内缓存Redis是独立的分布式缓存服务。Caffeine访问无网络开销速度更快Redis支持多实例共享保证数据全局一致性。第二层数据一致性。Caffeine每个实例一份数据更新时需通过消息通知或短过期时间解决实例间一致性问题。Redis天然集中所有实例读同一份数据一致性处理相对简单。第三层淘汰策略。重点讲Caffeine的Window-TinyLFU算法以及它相比LRU/LFU的优势。Redis本身也有内存淘汰策略比如allkeys-lru、volatile-ttl等可以把两者对照起来说体现你对缓存技术的系统性理解。还有一类高频面试题是“缓存穿透、缓存击穿、缓存雪崩怎么解决”这个题虽然老但结合Caffeine可以答出亮点穿透缓存空值或使用布隆过滤器。击穿Caffeine的single-flight机制天然解决单机击穿问题Redis场景可以用分布式锁或逻辑过期。雪崩给缓存过期时间加一个随机抖动避免大量key同时失效。5.3 我在实际项目中踩过的坑第一坑误用expireAfterAccess导致内存居高不下有一段时间我用Caffeine做用户在线状态缓存天真地选了expireAfterAccess(30分钟)心想“30分钟没人访问就过期”。结果活跃用户一直访问key一直在缓存里在线用户越多缓存越大高峰期老年代持续增长频繁触发Full GC。后来改成expireAfterWrite 每日定时清理逻辑问题才缓解。如果当时意识到expireAfterAccess是“访问续期”而不是“固定过期”就不会踩这个坑了。第二坑maxSize拍脑袋填内存被打爆有个业务我直接给Caffeine设了maximumSize(50_000)以为很安全。结果value是一个包含大量嵌套对象的详情实体单个占用几十KB5万个key累积下来就是几个GB直接撑爆堆内存。后来改用了maximumWeight按value的真实大小设置权重才算把内存控制住。再强调一次maximumSize的值不是拍脑袋定的要结合对象大小和堆内存预算计算。第三坑把refreshAfterWrite当成“自动更新”refreshAfterWrite这个配置容易被误解。它不是让你“数据定时变新”而是让“过期之后下次读时有机会触发异步刷新”。换句话说refreshAfterWrite设置后数据还是可能读到旧值只是系统会在后台异步加载新值。如果业务要求更新后立刻失效光靠refreshAfterWrite是远远不够的必须配合主动失效机制。我的一次真实经历配置了refreshAfterWrite(5分钟)但数据库里的数据改了缓存中还是旧值持续了5分钟才被刷新。产品找过来问为什么数据不对排查半天才意识到刷新不是即时更新。后来改成“更新DB后主动invalidate”问题立刻解决。第四坑分布式部署下只做本地缓存一个典型的多实例服务每个节点各自维护Caffeine所有节点都从同一个DB加载数据。结果用户请求路由到A节点时看到一个值路由到B节点时又看到另一个值因为两个节点的本地缓存过期时间不一致刷新时机不同步。这个问题的根治方式是在缓存更新时使用消息通知所有节点失效。如果你的系统不想引入MQ可以把本地缓存过期时间压短到1到2分钟把不一致窗口压缩到可接受范围但要明确告知业务方这是一个权衡方案。5.4 排查缓存问题的思路遇到缓存相关线上问题我一般按以下顺序排查先看命中率如果命中率骤降多半是key生成规则变了或缓存被整体清空了。再看驱逐次数如果驱逐次数很高说明容量配置偏小热点数据频繁被挤出。再查数据新鲜度如果数据是旧值优先确认更新时是否执行了主动失效。最后看GC和内存如果老年代增长明显检查缓存value对象大小和缓存容量。这套排查逻辑不是官方文档里写的而是我多次处理线上事故后总结出来的分享给大家参考。结尾缓存这块内容理论上看几篇文章人人都懂但真正到生产环境才发现细节里全是坑。从我个人的使用体会来说Caffein这个库本身的质量是过硬的但“缓存之王”这四个字不是靠库本身实现的而是靠合理选型、严谨配置和有效的一致性方案共同撑起来的。如果你的项目还没引入本地缓存或者还在用Guava Cache建议从一个小业务开始替换Caffeine压测对比一下命中率和延迟自己感受差异。如果你已经用了Caffeine但总遇到脏数据或内存问题回过头来检查一下你选了哪几个过期策略是主动失效还是只靠过期兜底这两个点解决了大部分问题都能烟消云散。最后分享一个小的实操技巧Caffeine的key尽量用基本类型或简单对象不要用复杂对象作为key否则toString和hashCode的计算开销会在高并发场景下被放大虽然单次看似微不足道但架不住量大。我一般习惯把所有缓存key统一拼成String比如user: userId又简单又能一眼看出业务关联。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询