当前位置: 首页 > news >正文

探究Redis + Caffeine两级缓存架构

上一篇博客浅谈了MyBatis的二级缓存,但不符合工程实践的要求。本篇博客将探究更符合生产实践的缓存架构——Redis + Caffeine两级缓存架构

1、caffeine

Redis大家一定熟悉,Caffeine比较陌生,它是基于 JAVA 8 的高性能本地缓存库,使用了Java 8最新的StampedLock锁技术,属于内存级本地缓存。spring 官方使用了 Caffeine 作为默认缓存组件。可参考github官方

淘汰策略:采用 Window-TinyLFU 算法,结合了 LRU(最近最少使用)和 LFU(最不经常使用)的优点。
滑动窗口分区: 缓存分为一个 主区域(保护高频数据)和一个 窗口区域(接纳新数据),避免突发流量污染缓存。
频率素描: 以极低的内存开销统计数据访问频率,替代传统 LFU 的哈希计数。
高性能并发设计:分段锁机制: 写操作分段加锁,减少线程竞争。无锁读优化: 通过 AtomicReference 实现并发读的高性能。

2、Redis+Caffeine实现两级缓存

2.1 依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency><dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.2.2</version></dependency>

2.2 配置类

RedisConfig

publicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryconnectionFactory){RedisTemplate<String,Object>redisTemplate=newRedisTemplate<>();redisTemplate.setConnectionFactory(connectionFactory);Jackson2JsonRedisSerializer<Object>serializer=newJackson2JsonRedisSerializer<>(Object.class);ObjectMappermapper=newObjectMapper();mapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);serializer.setObjectMapper(mapper);redisTemplate.setKeySerializer(newStringRedisSerializer());redisTemplate.setValueSerializer(serializer);redisTemplate.setHashKeySerializer(newStringRedisSerializer());redisTemplate.setHashValueSerializer(serializer);redisTemplate.afterPropertiesSet();returnredisTemplate;}}

CaffeineConfig

publicclassCaffeineConfig{@BeanpublicCache<String,Object>caffeineCache(){returnCaffeine.newBuilder().initialCapacity(128).maximumSize(1024).expireAfterWrite(60,TimeUnit.SECONDS).build();}@BeanpublicCacheManagercacheManager(){CaffeineCacheManagercacheManager=newCaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(128).maximumSize(1024).expireAfterWrite(60,TimeUnit.SECONDS));returncacheManager;}}

2.3 注解实现

DoubleCache 注解

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceDoubleCache{StringcacheName();String[]key();longexpireTime()default120;CacheTypetype()defaultCacheType.FULL;enumCacheType{FULL,PUT,DELETE}}

DoubleCacheAspect

@Slf4j@Component@AspectpublicclassDoubleCacheAspect{@ResourceprivateCachecaffeineCache;@ResourceprivateRedisTemplate<String,Object>redisTemplate;@Pointcut("@annotation(com.test.redis.annotation.DoubleCache)")publicvoiddoubleCachePointcut(){}@Around("doubleCachePointcut()")publicObjectdoAround(ProceedingJoinPointpoint)throwsThrowable{MethodSignaturesignature=(MethodSignature)point.getSignature();Methodmethod=signature.getMethod();String[]paramNames=signature.getParameterNames();Object[]args=point.getArgs();TreeMap<String,Object>treeMap=newTreeMap<>();for(inti=0;i<paramNames.length;i++){treeMap.put(paramNames[i],args[i]);}DoubleCacheannotation=method.getAnnotation(DoubleCache.class);StringelResult=DoubleCacheUtil.arrayParse(Lists.newArrayList(annotation.key()),treeMap);StringrealKey=annotation.cacheName()+":"+elResult;if(annotation.type()==DoubleCache.CacheType.PUT){Objectobject=point.proceed();redisTemplate.opsForValue().set(realKey,object,annotation.expireTime(),TimeUnit.SECONDS);caffeineCache.put(realKey,object);returnobject;}elseif(annotation.type()==DoubleCache.CacheType.DELETE){redisTemplate.delete(realKey);caffeineCache.invalidate(realKey);returnpoint.proceed();}ObjectcaffeineCacheObj=caffeineCache.getIfPresent(realKey);if(Objects.nonNull(caffeineCacheObj)){log.info("get data from caffeine");returncaffeineCacheObj;}ObjectredisCache=redisTemplate.opsForValue().get(realKey);if(Objects.nonNull(redisCache)){log.info("get data from redis");caffeineCache.put(realKey,redisCache);returnredisCache;}log.info("get data from database");Objectobject=point.proceed();if(Objects.nonNull(object)){log.info("get data from database write to cache: {}",object);redisTemplate.opsForValue().set(realKey,object,annotation.expireTime(),TimeUnit.SECONDS);caffeineCache.put(realKey,object);}returnobject;}}

切面中根据操作缓存的类型,分别处理更新、删除、二级缓存读取缓存操作。
更新缓存的操作,先执行原方法,再将value进行更新。
二级缓存读取缓存时,先读取caffeine本地缓存,再读取redis,若两者都没有,则执行原方法,执行后再将value进行更新,这就是第一次读取进行插入两级缓存的操作。

2.4 注解使用

service层的业务代码,可以加上@DoubleCache注解,进行缓存的查询、更新与删除操作。

@DoubleCache(cacheName="order",key="#id",type=CacheType.FULL)publicOrdergetOrderById(Longid){OrdermyOrder=orderMapper.selectOne(newLambdaQueryWrapper<Order>().eq(Order::getId,id));returnmyOrder;}@DoubleCache(cacheName="order",key="#order.id",type=CacheType.PUT)publicOrderupdateOrder(Orderorder){orderMapper.updateById(order);returnorder;}@DoubleCache(cacheName="order",key="#id",type=CacheType.DELETE)publicvoiddeleteOrder(Longid){orderMapper.deleteById(id);}

存入redis中的key示例为:
order:20260114000001234

其中的分隔符在YAML 格式 (application.yml)文件中可配置

cache:prefix:"my_project"# 全局前缀 separator:":"#<---分隔符

4、JetCache实现多级缓存(阿里开源,推荐生产)

JetCache 内置支持多级缓存,代码更简洁。生产环境、快速开发,复杂度低

  1. 添加依赖
<dependency><groupId>com.alicp.jetcache</groupId><artifactId>jetcache-starter-redis-lettuce</artifactId><version>2.7.5</version></dependency>
  1. 配置 application.yml
jetcache:statIntervalMinutes:15areaInCacheName:falselocal:default:type:caffeine keyConvertor:fastjson limit:1000remote:default:type:redis.lettuce keyConvertor:fastjson valueEncoder:java valueDecoder:java poolConfig:minIdle:5maxIdle:20maxTotal:50host:localhost port:6379
  1. 注解方式使用多级缓存
@CreateCache(name="order",expire=300,cacheType=CacheType.BOTH)privateCache<Long,User>userCache;publicUsergetUserById(Longid){//只有当缓存中 key=id 的数据【不存在】(null) 时,才会执行这个 Lambda 表达式returnuserCache.computeIfAbsent(id,(key)->{returnuserMapper.selectById(key);// 将查到的结果自动存入两级缓存 (本地 + 远程)// 返回结果});// 如果缓存【存在】,直接返回缓存值,根本不会执行上面的 Lambda,也不会查数据库}publicvoidupdateUser(Useruser){// 步骤 1: 更新数据库userMapper.updateById(user);// 步骤 2: 删除缓存 (关键步骤!)userCache.remove(user.getId());// 自动清除两级缓存}

对于缓存使用中常见的问题,可以有如下建议:
缓存穿透:对空结果缓存短 TTL(如 60 秒),或者缓存value为null
缓存雪崩:Redis 设置随机 TTL(如 30±5 分钟)
缓存击穿:Caffeine 本身抗高并发;Redis 可用互斥锁
数据一致性,更新时先更新 DB,再删除缓存(Cache-Aside 模式),随后在查询的时候,就会更新最新的缓存
分布式环境,本地缓存不一致,多实例部署时,本地缓存无法同步, 可通过 MQ 广播失效消息;此种做法较为复杂,一般靠 TTL 自愈;或者通过提供接口,手动调用刷新各个实例的本地缓存。

5、Caffeine长周期本地缓存实现

对于一个不经常变动的参数数据,比如说省份地区信息等,黑名单渠道信息,数据量通常不超过10000条,大小不超过10M,如果采用两级缓存的做法,会频繁查询数据库进而更新缓存,导致一定程度上耗时上升,可以采用Caffeine长周期本地缓存实现,比如黑名单渠道,Caffeine本地缓存查询不到,即不是黑名单渠道,这将防止缓存穿透到数据库的情况,极大提升业务处理的效率。

importcom.github.benmanes.caffeine.cache.Cache;importcom.github.benmanes.caffeine.cache.Caffeine;importorg.springframework.stereotype.Component;importjavax.annotation.PostConstruct;importjava.util.List;importjava.util.Set;importjava.util.concurrent.TimeUnit;importjava.util.stream.Collectors;/** * 黑名单渠道长周期本地缓存实现 * 适用于不常变动、小体量数据的缓存场景 */@ComponentpublicclassBlacklistChannelLocalCache{// Caffeine 缓存实例:Key-渠道ID,Value-渠道信息(此处简化为String)privateCache<String,String>blacklistChannelCache;privatefinalBlacklistChannelDaoblacklistChannelDao=newBlacklistChannelDao();/** * 初始化缓存:项目启动时加载全量数据,配置长周期缓存策略 */@PostConstructpublicvoidinitCache(){// 缓存配置:// 1. 最大容量10000条(适配业务数据量,最大约6M)// 2. 过期时间7天(长周期,可根据业务调整)// 3. 不设置自动刷新(数据不常变动,避免无效开销)blacklistChannelCache=Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(7,TimeUnit.DAYS).build();// 项目启动时加载全量黑名单渠道到缓存loadAllBlacklistChannelsToCache();}/** * 核心查询方法:判断渠道是否在黑名单中 * 缓存中查不到则直接判定为非黑名单,避免穿透到数据库 * @param channelId 渠道ID * @return true-黑名单,false-非黑名单 */publicbooleanisBlacklistChannel(StringchannelId){// 从缓存查询,null则表示非黑名单StringchannelInfo=blacklistChannelCache.getIfPresent(channelId);returnchannelInfo!=null;}/** * 手动刷新缓存(当数据变动时调用,如后台修改黑名单) * 避免长周期缓存导致数据更新不及时 */publicvoidrefreshCache(){// 清空旧缓存blacklistChannelCache.invalidateAll();// 重新加载全量数据loadAllBlacklistChannelsToCache();}/** * 加载全量黑名单渠道到缓存 */privatevoidloadAllBlacklistChannelsToCache(){// 从数据源查询全量数据List<BlacklistChannel>allChannels=blacklistChannelDao.queryAllBlacklistChannels();// 批量放入缓存Set<String>channelIds=allChannels.stream().map(BlacklistChannel::getChannelId).collect(Collectors.toSet());for(BlacklistChannelchannel:allChannels){blacklistChannelCache.put(channel.getChannelId(),channel.getChannelInfo());}System.out.println("缓存初始化完成,加载黑名单渠道数量:"+channelIds.size());}// ===================== 业务类=====================/** * 黑名单渠道实体 */staticclassBlacklistChannel{privateStringchannelId;// 渠道IDprivateStringchannelInfo;// 渠道信息(如名称、类型等)// 构造器、getter/setterpublicBlacklistChannel(StringchannelId,StringchannelInfo){this.channelId=channelId;this.channelInfo=channelInfo;}publicStringgetChannelId(){returnchannelId;}publicStringgetChannelInfo(){returnchannelInfo;}}/** * 数据库访问层Dao,用于测试 */staticclassBlacklistChannelDao{/** * 查询全量黑名单渠道 */publicList<BlacklistChannel>queryAllBlacklistChannels(){returnList.of(newBlacklistChannel("channel_001","违规渠道A"),newBlacklistChannel("channel_002","违规渠道B"),newBlacklistChannel("channel_003","违规渠道C"));}}// ===================== 测试方法 =====================publicstaticvoidmain(String[]args){BlacklistChannelLocalCachecache=newBlacklistChannelLocalCache();cache.initCache();// 测试1:查询黑名单渠道System.out.println("channel_001 是否黑名单:"+cache.isBlacklistChannel("channel_001"));// true// 测试2:查询非黑名单渠道(缓存无数据,直接返回false,不查库)System.out.println("channel_999 是否黑名单:"+cache.isBlacklistChannel("channel_999"));// false// 测试3:刷新缓存cache.refreshCache();}}

1. 缓存初始化(initCache):
使用 @PostConstruct 注解,项目启动时自动执行,避免运行时首次查询触发数据库访问;
若使用 Spring 框架,使用 @Component、@PostConstruct 注解能被扫描到;
配置 maximumSize(10000) 限制缓存容量,适配业务数据量;
expireAfterWrite(7, TimeUnit.DAYS) 设置 7 天过期,实现 “长周期” 缓存,减少更新频率。
此处通常配合一个批量,在7天的周期内进行刷新,比如批量设置6天定期刷新一次。
2. 核心查询方法(isBlacklistChannel):
仅从缓存查询(getIfPresent),不调用数据库;
缓存中查不到则直接返回 false,彻底避免缓存穿透到数据库,符合 “查询不到即非黑名单” 的业务逻辑。
3. 缓存刷新(refreshCache):
提供手动刷新入口,当数据变动(如后台修改黑名单)时调用,解决长周期缓存的 “数据新鲜度” 问题;
先清空旧缓存再重新加载,避免新旧数据混用。
4. 防穿透设计:
核心逻辑是 “缓存未命中 = 非黑名单”,无需像传统缓存那样 “查库后更新缓存”,从根源上杜绝穿透;
数据量小(≤10M),全量加载到内存无性能压力。

Caffeine长周期本地缓存实现总结
核心设计:长周期缓存 + 启动全量加载 + 缓存未命中直接判定,避免数据库访问和缓存穿透;
关键特性:配置合理的容量和过期时间,提供手动刷新入口平衡 “长周期” 和 “数据新鲜度”;
性能优势:纯内存查询(Caffeine 性能接近 HashMap),查询耗时微秒级,远高于数据库查询。
该实现可直接适配省份地区、黑名单渠道等小体量、低频变动数据的缓存场景,极大提升业务处理效率。

4、总结

本文首先介绍了caffeine的原理,通过手写Redis+Caffeine实现两级缓存进一步理解了缓存的用法,在实际生产项目中,更推荐JetCache实现多级缓存,最后介绍了在一些特殊场景中推荐使用的本地缓存的做法。缓存是一把双刃剑,提升了查询效率。redis的无故障时间虽然很少,但也存在故障的可能性,因此在项目设计过程中,应采用冗余兜底策略,使查询的数据高效准确。

参考:
https://blog.csdn.net/weixin_43887285/article/details/147238521
https://www.jb51.net/database/347796681.htm
https://cloud.tencent.com/developer/article/2357414
https://blog.csdn.net/LearnerDL/article/details/138173159

http://www.cnnetsun.cn/news/1288553.html

相关文章:

  • Qwen3-0.6B-FP8保姆级教程:修复Chainlit CORS错误、WebSocket连接失败等高频问题
  • Qwen3-ASR-1.7B镜像免配置教程:一键切换CPU模式(低负载调试)与GPU模式(生产部署)
  • MiniCPM-V-2_6视频理解作品:10秒短视频自动生成含时间戳的详细字幕
  • Jimeng AI Studio效果展示:LoRA风格迁移能力——人物肖像跨风格转换案例
  • Phi-3-Mini-128K开源镜像部署:中小企业低成本AI助手落地实践
  • Qwen3-ASR-0.6B效果展示:长音频(30分钟)流式识别稳定性与断句准确性
  • ImportError: cannot import name ‘OpenAI‘ from ‘openai‘ 解决方案
  • Git-RSCLIP开源模型部署:支持国产昇腾NPU的适配进展与实测数据
  • RMBG-2.0职业教育应用:实训设备图透明背景用于在线课程建设
  • 如何优化ComfyUI加载时间?模型预加载部署技巧
  • 【LINUX】如何将deb包解压和封装
  • 本地部署国产openclaw(CoPaw)(保姆级图文讲解)
  • YOLOv10改进策略【卷积层】| ICCV 2025 UniConvNet 感受野聚合器RFA 小核组合扩ERF + AGD保持提表征,兼顾精度与效率
  • 手把手教你把 Gemini CLI 塞进 IntelliJ IDEA:ACP 集成指南
  • 力扣第73题:柱形图中最大的矩形
  • AI Agents as Universal Task Solvers: It’s All About Time
  • 网安保研避坑指南:中科院信工所各研究室真实体验与导师选择建议
  • Ollama + OpenClaw 本地AI助手实战:无需API Key的完全离线解决方案
  • 基于Hunyuan-MT-7B的Java开发实战:SpringBoot微服务集成指南
  • Unsloth实战演练:从零开始微调一个中文对话模型全过程
  • 基于鹈鹕算法优化支持向量机的数据分类预测(POA-SVM)在MATLAB环境下的应用
  • 突破限制:RenderDoc 调试 WebGL 应用的实战与替代方案
  • 华为---OSPF多区域网络设计与配置实战
  • Vue3 + vxe-table 实战:5分钟搞定财务凭证打印功能(含完整代码)
  • ESP8685-WROOM-01 全链路工程指南:RISC-V+Wi-Fi+BLE工业级落地
  • TOTOLink T6路由器漏洞实战:从Telnet开启到MQTT命令注入全流程解析
  • 从高考保险柜到乐观锁:AtomicStampedReference如何根治CAS的ABA顽疾
  • Linux网络驱动开发:PHY状态机与链路检测机制详解(附实战代码分析)
  • HY-Motion 1.0效果实测:对比手动K帧,AI生成动作自然度如何?
  • 零代码实战:用Clawdbot在星图平台快速部署Qwen3-VL:30B并接入飞书(保姆级教程)