微服务多级缓存架构设计

发布时间:2026/10/11 16:38:25

微服务多级缓存架构设计 1 需求背景系统读多写少场景大量热点字典、基础业务信息请求全部打到 RedisRedis CPU / 带宽压力高。 引入本地内存缓存缩短访问链路同时解决多实例本地缓存脏数据问题。非目标不用于强一致性业务库存、余额这类数据禁止使用 L1 本地缓存不替代数据库只做查询加速2 总体架构2.1 层级定义L1Caffeine JVM 堆内存缓存纳秒级读取单机隔离弱一致性TTL 兜底热点只读数据L2Redis 分布式缓存毫秒级多实例共享L3MySQL源数据2.2 读写流程读流程查询 L1 本地缓存命中直接返回L1 未命中查询 Redis命中则回填 L1 并返回Redis 未命中查询数据库DB 结果回填 RedisL1返回结果DB 不存在数据写入空标记到 Redis短 TTL防止缓存穿透写流程Cache Aside更新数据库删除 Redis 对应 keyRedis 发布失效广播消息所有应用实例收到消息删除本机 L1 缓存原则更新 DB删缓存不更新缓存。2.3 一致性策略一致性模型最终一致性允许短暂脏数据保障手段失效广播 L1 短 TTL 兜底消息丢失时脏数据最多存活 L1 TTL 时长强一致业务禁用 L1 本地缓存只使用 Redis3 缓存数据分类与准入规范表格数据类型是否启用 L1示例静态基础字典✅地区、枚举、配置低频变更热点业务数据✅商品基础信息高频变更业务数据❌订单状态、库存冷数据❌历史归档数据4 缓存策略设计4.1 TTL 设计L1 Caffeine写入过期推荐 2~5minRedis基础 TTL 随机偏移防止雪崩推荐 30min 0~300s 随机空值缓存短 TTL30s4.2 缓存问题防护方案缓存穿透DB 不存在数据写入空标记超大 key 集合场景增加布隆过滤器缓存击穿热点 key 采用后台异步刷新或使用 Redis 分布式锁单请求加载 DB缓存雪崩TTL 随机Redis 集群服务熔断降级本地缓存容量上限防 OOM4.3 本地缓存失效方案方案Redis Pub/Sub 广播失效消息缺点PubSub 消息不可靠消息丢失会产生脏数据必须搭配 L1 短 TTL 兜底高可靠场景替换为 RocketMQ 消息做失效通知5 组件设计5.1 MultiCacheManager 核心组件能力多级 get、缓存失效 evict、缓存统计埋点、本地缓存开关降级不直接复用 Spring Cache 注解自定义封装原生 Spring Cache 不支持多级联动失效广播。5.2 缓存 Key 规范格式cache:{业务域}:{key}示例cache:user:15.3 序列化方案使用 FastJSON2 序列化对象禁止 JDK 序列化。6 监控指标设计表格指标说明告警阈值caffeine.hitRateL1 缓存命中率低于 70% 告警redis.hitRateRedis 缓存命中率低于 60% 告警cache.pass.through.count缓存穿透次数突增告警cache.invalid.msg.fail失效消息消费失败0 告警redis.cpuRedis CPU 使用率80% 告警7 实施与上线流程业务梳理识别热点数据划分是否允许 L1 缓存代码开发接入 MultiCacheManager埋点监控单元测试验证命中、失效、穿透场景压测高并发验证击穿、雪崩灰度上线小流量开启配置开关可一键关闭 L1全量上线持续监控指标8 风险与应急预案风险清单本地缓存脏数据PubSub 消息丢失预案配置中心一键关闭 L1 本地缓存降级为纯 RedisCaffeine 内存溢出预案强制配置 maximumSize限制缓存条目上限Redis 故障预案熔断直接访问 DB限流保护大 key禁止大对象存入缓存大 key 拆分9 使用约束高频更新数据禁止开启 L1 本地缓存强一致性业务禁用 L1L1 缓存必须设置 maxSize防止 OOM不使用缓存更新策略统一删缓存L1 TTL 不能设置过长10 后续优化方向热点 key 异步刷新布隆过滤器接入拦截不存在 keyMQ 替换 PubSub实现可靠失效通知接入 Prometheus 指标埋点缓存预热组件1. SpringBoot 完整可运行多级缓存 Demo技术栈SpringBoot 2.7.x/ 3.xCaffeineSpring Data RedisRedisson可选分布式锁Redis PubSub 做本地缓存失效广播 能力L1Caffeine 本地缓存L2Redis 分布式缓存Cache Aside空值防穿透缓存埋点开关控制本地缓存Redis PubSub 失效广播可一键关闭 L1 降级。 注意SpringBoot3 需要使用jakarta包下面代码默认 SpringBoot2按需替换。Maven pom.xml?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.15/version relativePath/ /parent groupIdcom.cache/groupId artifactIdmulti-level-cache-demo/artifactId version0.0.1-SNAPSHOT/version namemulti-level-cache-demo/name dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.github.benmanes.caffeine/groupId artifactIdcaffeine/artifactId version3.1.8/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson2/artifactId version2.0.32/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies /projectapplication.ymlspring: redis: host: 127.0.0.1 port: 6379 password: database: 0 multi-cache: # 本地缓存开关生产故障时一键关闭L1 enable-local-cache: true # Caffeine 最大缓存条目 caffeine-max-size: 10000 # Caffeine 写入后过期时间单位秒 caffeine-expire-write: 120 # Redis基础过期时间单位秒 redis-base-ttl: 1800 # Redis随机偏移防止雪崩单位秒 redis-random-ttl: 300 # 空值缓存TTL防穿透 null-cache-ttl: 30 cache-topic: multi:cache:invalidate:topic配置类MultiCacheConfig.javapackage com.cache.config; import com.alibaba.fastjson2.JSON; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.concurrent.TimeUnit; Slf4j Configuration public class MultiCacheConfig { Value(${multi-cache.enable-local-cache}) private boolean enableLocalCache; Value(${multi-cache.caffeine-max-size}) private long caffeineMaxSize; Value(${multi-cache.caffeine-expire-write}) private long caffeineExpireWrite; Value(${cache-topic}) private String cacheInvalidTopic; /** * Caffeine L1本地缓存 */ Bean public com.github.benmanes.caffeine.cache.CaffeineObject, Object caffeineBuilder() { return Caffeine.newBuilder() .maximumSize(caffeineMaxSize) .expireAfterWrite(caffeineExpireWrite, TimeUnit.SECONDS) .recordStats(); } Bean public RedisTemplateString, String redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, String redisTemplate new RedisTemplate(); redisTemplate.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); redisTemplate.setKeySerializer(stringSerializer); redisTemplate.setValueSerializer(stringSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } /** * Redis Pub/Sub 消息容器监听缓存失效消息 */ Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(listenerAdapter, new ChannelTopic(cacheInvalidTopic)); return container; } Bean public MessageListenerAdapter messageListenerAdapter(CacheInvalidMessageHandler messageHandler) { return new MessageListenerAdapter(messageHandler, onMessage); } }失效消息处理器CacheInvalidMessageHandler.javapackage com.cache.config; import com.github.benmanes.caffeine.cache.Cache; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; Slf4j Component public class CacheInvalidMessageHandler { Autowired private CacheObject, Object caffeineCache; /** * 收到广播消息清除本机L1缓存 */ public void onMessage(String key) { log.info(收到缓存失效广播清除本地缓存key:{}, key); caffeineCache.invalidate(key); } }多级缓存核心工具类MultiCacheManager.javapackage com.cache.manager; import com.alibaba.fastjson2.JSON; import com.github.benmanes.caffeine.cache.Cache; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; Slf4j Component public class MultiCacheManager { private static final String NULL_FLAG NULL_CACHE; Autowired private CacheObject, Object caffeineCache; Autowired private RedisTemplateString, String redisTemplate; Value(${multi-cache.enable-local-cache}) private boolean enableLocalCache; Value(${multi-cache.redis-base-ttl}) private long redisBaseTtl; Value(${multi-cache.redis-random-ttl}) private long redisRandomTtl; Value(${multi-cache.null-cache-ttl}) private long nullCacheTtl; Value(${cache-topic}) private String cacheInvalidTopic; private final Random random new Random(); /** * 多级缓存查询 * param key 缓存key * param clazz 返回对象类型 * param dbLoader 数据库查询逻辑 * return 业务对象 */ public T T get(String key, ClassT clazz, SupplierT dbLoader) { //1.优先读取L1本地缓存 if (enableLocalCache) { Object localVal caffeineCache.getIfPresent(key); if (localVal ! null) { log.debug(L1缓存命中 key{}, key); return (T) localVal; } } //2.读取Redis L2缓存 String redisStr redisTemplate.opsForValue().get(key); if (redisStr ! null) { if (NULL_FLAG.equals(redisStr)) { log.debug(Redis命中空缓存 key{}, key); return null; } T val JSON.parseObject(redisStr, clazz); //回填本地缓存 if (enableLocalCache) { caffeineCache.put(key, val); } log.debug(Redis缓存命中 key{}, key); return val; } log.debug(缓存全部未命中查询DB key{}, key); //3.查询数据库 T dbData dbLoader.get(); if (dbData null) { //写入空值防穿透 redisTemplate.opsForValue().set(key, NULL_FLAG, nullCacheTtl, TimeUnit.SECONDS); return null; } //4.回填Redis增加随机TTL防止雪崩 long realTtl redisBaseTtl random.nextLong(redisRandomTtl); redisTemplate.opsForValue().set(key, JSON.toJSONString(dbData), realTtl, TimeUnit.SECONDS); //回填本地缓存 if (enableLocalCache) { caffeineCache.put(key, dbData); } return dbData; } /** * 更新/删除数据后失效缓存 * param key 缓存key */ public void evict(String key) { // 删除Redis缓存 redisTemplate.delete(key); // 删除本机本地缓存 if (enableLocalCache) { caffeineCache.invalidate(key); } //广播失效消息通知其他应用实例清本地缓存 redisTemplate.convertAndSend(cacheInvalidTopic, key); log.info(缓存失效完成广播消息 key{}, key); } /** * 获取缓存统计信息用于监控 */ public String getCacheStats() { if (!enableLocalCache) { return local cache disabled; } return caffeineCache.stats().toString(); } }测试实体User.javapackage com.cache.entity; import lombok.Data; Data public class User { private Long id; private String username; private String phone; }Service 测试层UserService.javapackage com.cache.service; import com.cache.entity.User; import com.cache.manager.MultiCacheManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; Service RequiredArgsConstructor public class UserService { private final MultiCacheManager multiCacheManager; //模拟DB public User getById(Long id) { String cacheKey cache:user: id; return multiCacheManager.get(cacheKey, User.class, () - { //模拟数据库查询逻辑 if (id 1L) { User user new User(); user.setId(1L); user.setUsername(test); user.setPhone(13800138000); return user; } return null; }); } public void updateUser(Long id) { //1.更新DB省略 //2.失效缓存 String cacheKey cache:user: id; multiCacheManager.evict(cacheKey); } }Controller 测试入口CacheTestController.javapackage com.cache.controller; import com.cache.entity.User; import com.cache.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; RestController RequiredArgsConstructor public class CacheTestController { private final UserService userService; GetMapping(/user/get) public User getUser(RequestParam Long id) { return userService.getById(id); } PostMapping(/user/update) public String updateUser(RequestParam Long id) { userService.updateUser(id); return ok; } GetMapping(/cache/stats) public String stats() { return userService.getCacheStats(); } }Demo 说明多实例启动时更新接口触发evictRedis 发布消息所有实例收到消息清除本机 Caffeineenable-local-cache配置项可以关闭 L1降级为纯 Redis 缓存空值缓存防止穿透Redis TTL 随机防雪崩没有内置分布式锁热点 key 击穿场景可自行在 get 方法增加 Redisson 锁
延伸阅读

更多相关文章

2026/10/11 16:38:25

安全日志分析实战:从撞库、Webshell到横向移动的攻击链还原方法

做安全运营这些年,我翻过的日志如果打印出来,大概能堆满一整面墙。网络攻击日志分析这件事,听起来很高大上,实际干起来往往是从一堆看似无关的字符里,把攻击者的行动轨迹一点点抠出来。你盯着几十万行访问记录&#xf…

2026/10/11 16:38:24

从SEO到GEO:AI时代企业为什么需要建立品牌知识资产?

随着生成式AI快速进入企业营销体系,传统的搜索流量逻辑正在出现新的变化。 世界广告主联合会(WFA)最新调研显示,96%的受访大型品牌已经在使用生成式AI或智能体AI。 对于企业数字化团队而言,一个值得关注的问题是&#…

2026/10/11 17:43:27

手写文字去除:OCR前图像预处理的可控方案

简介:本资源提供手写文字智能擦除的工业级Python实现方案,面向图像处理开发者、AI算法工程师及教育信息化从业者,解决试卷、表单等场景中手写内容与印刷体混杂导致的OCR识别干扰问题。资源包共36个文件,含22个核心Python脚本&…

2026/10/11 17:43:27

响应头里藏flag?从“迅疾响应”看CTF信息收集与源码审计

前几天一个刚入门的朋友在CTFshow上刷“菜狗杯”的新手题,卡在了一道叫“迅疾响应”的题目上。他跑来跟我说:页面就一行字,啥提示都没有,不知道从哪下手。我让他把浏览器开发者工具打开,先别碰页面内容,去翻…

2026/10/11 17:43:27

GemPy隐式地质建模实战:从数据准备到MCMC不确定性分析

简介:GemPy是基于Python的开源隐式3D结构地质建模库,它利用界面与方向数据自动构建褶皱、断层网络和不整合面等复杂地质结构,避免了传统显式建模的繁杂几何操作,并支持贝叶斯推断与蒙特卡洛随机模拟以量化参数和模型不确定性&…

2026/10/11 17:43:27

Sourcetree重置操作全解析:soft、mixed、hard三种模式与恢复技巧

1. 重置操作到底在重置什么:先搞懂Git的“后悔药”体系在聊Sourcetree之前,先说说我为什么总会写到这个主题。Git用久了你会发现,真正让人心里发虚的操作往往不是冲突合并,而是“怎么回到过去”。提交记录删不掉、分支拉错、本地改…

2026/10/11 17:43:27

Matlab CNN图像分类实战:从数据流水线到参数调试完整指南

简介:这是一套基于Matlab实现的卷积神经网络图像分类项目,适合正在学习CNN原理、需要完整可运行代码进行实验的初学者,也适合有一定深度学习基础的开发人员快速迁移使用。项目覆盖数据加载、特征提取(PCA)、网络初始化…

2026/10/11 17:38:27

SpringBoot+微信小程序点餐系统实战:从架构到支付回调避坑指南

如果你最近在调研“小程序点餐系统”这类题目,大概率会看到一堆千篇一律的项目骨架:用户登录、商品列表、下单、支付,没了。但真正到了答辩或者上线阶段,才会发现购物车并发、库存扣减、微信支付回调、小程序体验版配置这些才是拉…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑