很多同学做微服务,一提缓存就只想到 Redis—— 不是说 Redis 不好,而是单靠它,就像穿一只鞋跑步,跑不远还容易摔。咱们先掰扯下常见的 “单级缓存误区”,看看你是不是也踩过。
兄弟们,上周四半夜三点,朋友发来消息:“哥救急!”,后面跟着一串截图:线上微服务接口超时率飙到 30%,数据库 CPU 干到 100%,运维小哥已经在群里 @他八百次了。
我让他先把接口监控发过来,一眼就看出来问题:商品列表接口没加缓存,用户一点开 APP,所有请求直接扎进数据库,就像春运的时候所有人都挤一个检票口,不堵才怪。后来给他加了个多级缓存,半小时不到,接口响应时间从 500ms 降到 20ms,数据库 CPU 直接掉到 10% 以下。 这就是今天要跟大家聊的 “微服务 + 多级缓存”,不是什么高深黑科技,但用好了是真能救命。
一、先吐槽下:单级缓存就是 “瘸腿走路”
很多同学做微服务,一提缓存就只想到 Redis—— 不是说 Redis 不好,而是单靠它,就像穿一只鞋跑步,跑不远还容易摔。咱们先掰扯下常见的 “单级缓存误区”,看看你是不是也踩过。
1. 只靠本地缓存:像家里冰箱只能自己用
有些同学图省事,在服务里用个 HashMap 当本地缓存(更讲究点的用 Guava Cache),确实快 —— 毕竟是内存操作,比查数据库快 100 倍都不止。但问题来了:微服务不是单台机器跑啊!
你部署 10 台服务实例,每台机器的本地缓存都是 “独立王国”。比如商品价格改了,你只更了其中一台的缓存,剩下 9 台还存着旧价格,用户刷到的价格一会儿高一会儿低,客服电话能被打爆。更坑的是,如果某台机器缓存里的热点数据过期了,所有请求会突然全扎进这台机器的数据库,直接把它干崩(这叫 “缓存击穿” 的局部版)。
简单说:本地缓存是 “自家冰箱”,只能自己用,邻居(其他实例)用不上,还容易藏 “过期食物”(旧数据)。
2. 只靠 Redis:网络是个 “隐形杀手”
更多同学会选 Redis 当分布式缓存,毕竟能跨实例共享数据,还能抗高并发。但你有没有算过一笔账:Redis 再快,也是 “远程调用”—— 从服务实例发请求到 Redis,再等 Redis 返回,这中间的网络开销可不小。
举个真实例子:之前帮一个电商项目调优,商品详情接口用了 Redis 缓存,响应时间大概 80ms。后来加了本地缓存(Caffeine),同样的接口直接降到 15ms—— 差了 5 倍多!为啥?因为本地缓存不用走网络,直接读内存,就像你从口袋里掏手机,比从快递站取快递快多了。
更要命的是 Redis 也会 “累”。比如秒杀活动,每秒几万请求打过来,就算 Redis 能扛住,网络带宽也可能被占满,后面正常请求全卡住。这时候要是再遇到缓存雪崩(大量 key 同时过期),所有请求一起冲去数据库,那场面,数据库直接 “原地去世”。
3. 结论:多级缓存是 “组合拳”,不是 “单选题”
单级缓存的问题本质是:本地缓存缺 “共享”,Redis 缺 “速度”。那解决办法就很简单了 —— 把两者结合起来,再加上网关层的缓存(比如 Nginx),搞个 “多级缓存”,让请求像走 “过滤网” 一样,一层一层被挡住,最后漏到数据库的请求就没几个了。
就像小区安保:先看大门(网关缓存),不是小区的直接拦;进了大门看单元门(本地缓存),住户直接进;单元门没卡,再查物业登记(Redis);最后实在不行才找业主确认(数据库)—— 这样效率才高,还不容易出乱子。
二、多级缓存怎么搭?从 “三层架构” 讲透
咱们聊最实用的 “三级缓存架构”:网关缓存(Nginx)→ 本地缓存(Caffeine)→ 分布式缓存(Redis)。不是说必须三层都上,小项目可能本地 + Redis 就够了,大项目再补个网关缓存,按需搭配。
先给个整体流程图,后面逐个拆解:
用户请求 → Nginx网关(查网关缓存)→ 有就返回
↓ 没有
微服务实例(查本地缓存Caffeine)→ 有就返回
↓ 没有
Redis分布式缓存 → 有就返回(同时回写本地缓存)
↓ 没有
数据库 → 查询结果(同时回写Redis和本地缓存)→ 返回
1. 第一层:网关缓存(Nginx)——“大门卫”,拦高频静态请求
网关是请求进入微服务的第一站,用 Nginx 做缓存,主要拦那些 “不怎么变” 的静态请求,比如商品分类列表、首页 Banner 图这些。
为啥用 Nginx?因为它比 Java 服务轻量,抗并发能力更强 ——Java 服务单机撑几千 QPS 就不错了,Nginx 轻松上万。而且请求不用进 Java 服务,直接在 Nginx 层面返回,效率高到飞起。
实战配置:Nginx 缓存静态接口
比如要缓存 “/api/v1/category/list” 这个分类接口,Nginx 配置大概长这样:
http {
# 定义缓存区:名字叫micro_cache,内存100M,过期时间10分钟
proxy_cache_path /var/nginx/cache levels=1:2 keys_zone=micro_cache:100m inactive=10m max_size=1g;
server {
listen 80;
server_name api.yourecommerce.com;
location /api/v1/category/list {
# 启用缓存,用上面定义的micro_cache
proxy_cache micro_cache;
# 缓存key:用请求URI+参数,避免不同请求混了
proxy_cache_key $uri$is_args$args;
# 200和304状态码缓存10分钟
proxy_cache_valid 200 304 10m;
# 缓存命中率这些信息,加在响应头里,方便监控
add_header X-Cache-Status $upstream_cache_status;
# 转发到微服务集群
proxy_pass http://micro_service_cluster;
}
}
}
注意点:别瞎缓存动态接口
Nginx 缓存适合 “纯静态、少变化” 的接口,比如分类、Banner。像用户订单、购物车这种 “每个人都不一样” 的动态接口,千万别用 Nginx 缓存 —— 不然张三能看到李四的订单,那就等着背锅吧。
如果非要缓存动态接口,得在 key 里加用户标识,比如proxy_cache_key $uri$is_args$args$cookie_user_id;,但这样缓存命中率会很低,不如不用,所以一般不推荐。
2. 第二层:本地缓存(Caffeine)——“贴身管家”,快到离谱
本地缓存是微服务实例自己的 “内存缓存”,用 Caffeine(Guava Cache 的升级版)最合适 —— 性能比 Guava 好,配置还灵活,现在 Java 项目基本都用它。
它的核心优势就一个字:快!不用走网络,直接读 JVM 内存,响应时间能做到毫秒级甚至微秒级。适合存那些 “高频访问、短期不变” 的数据,比如商品详情、热门商品列表。
第一步:Spring Boot 集成 Caffeine(直接抄代码)
先加依赖(Maven):
<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.1.8</version> <!-- 用最新版就行 -->
</dependency>
然后写个配置类,定义缓存规则:
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching // 开启缓存注解支持
public class CaffeineConfig {
// 定义缓存管理器:不同缓存用不同规则
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 1. 商品详情缓存:最多存1000个,10分钟过期
Caffeine<Object, Object> productCache = Caffeine.newBuilder()
.maximumSize(1000) // 最大缓存数量(满了会删最少用的)
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.recordStats(); // 记录缓存命中率(方便监控)
// 2. 热门商品缓存:最多存500个,5分钟过期(更新更频繁)
Caffeine<Object, Object> hotProductCache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats();
// 把不同缓存规则加进去,用名字区分
cacheManager.setCaffeineMap(Map.of(
"productDetailCache", productCache,
"hotProductCache", hotProductCache
));
return cacheManager;
}
}
然后在 Service 层用注解就能用:
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// 查商品详情:用productDetailCache缓存
@Cacheable(value = "productDetailCache", key = "#productId")
public ProductVO getProductDetail(Long productId) {
// 这里查数据库,没缓存时才会执行
ProductDO productDO = productMapper.selectById(productId);
// 转成VO返回
return convertToVO(productDO);
}
// 查热门商品:用hotProductCache缓存
@Cacheable(value = "hotProductCache", key = "'hotList'") // 固定key,因为是列表
public List<ProductVO> getHotProductList() {
return productMapper.selectHotProductList();
}
}
第二步:Caffeine 核心配置解读(别瞎配)
很多同学用 Caffeine 只知道设过期时间,其实几个关键参数能决定缓存效果:
- maximumSize:最大缓存数量,必须设!不然缓存会无限膨胀,最后 JVM 内存爆了,运维小哥会提着刀找你。
- expireAfterWrite:写入后过期时间,适合数据会变的场景(比如商品价格)。
- expireAfterAccess:访问后过期时间,适合 “不用就过期” 的场景(比如用户会话)。
- recordStats:记录命中率,一定要开!后面监控用,命中率低于 80% 就得调缓存策略了。
第三步:Caffeine 底层为啥快?(稍微深入点)
Caffeine 用的是 “W-TinyLFU” 算法,比传统的 LRU(最近最少使用)聪明多了。举个例子:
LRU 就像你整理衣柜,只把最久没穿的衣服扔掉。但有时候你刚买的新衣服(最近用了一次),因为之前没穿过,会被 LRU 当成 “久没穿” 的扔掉 —— 这就很蠢。
W-TinyLFU 会 “记仇”:它会统计每个 key 的访问次数,哪怕是新 key,只要最近访问过,就不会轻易扔掉。简单说,它既照顾 “最近用的”,又照顾 “经常用的”,缓存命中率比 LRU 高不少。
3. 第三层:分布式缓存(Redis)——“共享仓库”,跨实例通用
Redis 是多级缓存的 “中坚力量”,主要解决本地缓存 “不共享” 的问题。比如你有 10 台服务实例,本地缓存各存各的,Redis 就是那个 “共享仓库”,让所有实例都能拿到最新数据。
这部分重点不是教你怎么用 Redis(毕竟大家基本都会),而是讲怎么避坑 —— 缓存穿透、击穿、雪崩这三大难题,必须解决,不然 Redis 加了也白加。
第一步:Spring Boot 集成 Redis(基础操作)
先加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 用Redisson,功能更多,比如分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
配置文件(application.yml):
spring:
redis:
host: 192.168.1.100
port: 6379
password: your_redis_password
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 2 # 最小空闲连接
timeout: 2000ms # 超时时间
redisson:
singleServerConfig:
address: redis://192.168.1.100:6379
password: your_redis_password
connectionMinimumIdleSize: 2
connectionPoolSize: 8
然后用 RedisTemplate 或者 @Cacheable(和 Caffeine 类似),这里推荐用 Redisson,因为它自带分布式锁、布隆过滤器这些工具,后面解决问题要用。
第二步:三大难题解决方案(实战版)
这部分是重点,咱们一个个来,每个问题都给 “能直接用的代码”。
1. 缓存穿透:故意查不存在的数据,绕开缓存打数据库
比如有人故意查productId=-1,这个 ID 在数据库里根本没有,所以缓存里也没有,每次请求都会扎进数据库 —— 如果每秒几千个这种请求,数据库直接扛不住。
解决方案:布隆过滤器 + 缓存空值
- 布隆过滤器:像小区门禁,先判断这个 ID 在不在数据库里,不在就直接返回,不用查缓存和数据库。
- 缓存空值:就算布隆过滤器漏了,查数据库没找到,也往缓存里存个 “空值”(比如null),过期时间设短点(比如 1 分钟),避免下次再查。
代码实现(用 Redisson 布隆过滤器):
先初始化布隆过滤器(项目启动时执行):
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Component
public class BloomFilterInit implements CommandLineRunner {
@Resource
private RedissonClient redissonClient;
@Resource
private ProductMapper productMapper;
// 布隆过滤器名字
private static final String PRODUCT_BLOOM_FILTER = "productBloomFilter";
// 预计数据量(比如100万商品)
private static final long EXPECTED_INSERTIONS = 1000000;
// 误判率(越小越费内存,一般设0.01就行)
private static final double FALSE_POSITIVE_RATE = 0.01;
@Override
public void run(String... args) throws Exception {
// 获取布隆过滤器
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(PRODUCT_BLOOM_FILTER);
// 初始化:如果没初始化过才执行
if (!bloomFilter.isExists()) {
bloomFilter.tryInit(EXPECTED_INSERTIONS, FALSE_POSITIVE_RATE);
// 把数据库里所有商品ID加载到布隆过滤器
List<Long> allProductIds = productMapper.selectAllProductIds();
allProductIds.forEach(bloomFilter::add);
}
}
}
然后在 Service 层用:
@Service
publicclass ProductService {
@Resource
private RedissonClient redissonClient;
@Resource
private ProductMapper productMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
// 查商品详情(防穿透版)
public ProductVO getProductDetail(Long productId) {
String cacheKey = "product:detail:" + productId;
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("productBloomFilter");
// 1. 先过布隆过滤器:不在就直接返回
if (!bloomFilter.contains(productId)) {
returnnull; // 或者返回“商品不存在”
}
// 2. 查Redis缓存
String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
// 缓存有值:如果是空值,返回null;否则转VO
return"null".equals(cacheValue) ? null : JSON.parseObject(cacheValue, ProductVO.class);
}
// 3. 查数据库
ProductDO productDO = productMapper.selectById(productId);
if (productDO == null) {
// 数据库没找到,缓存空值,1分钟过期
stringRedisTemplate.opsForValue().set(cacheKey, "null", 1, TimeUnit.MINUTES);
returnnull;
}
// 4. 数据库找到,缓存真实数据,30分钟过期
ProductVO productVO = convertToVO(productDO);
stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(productVO), 30, TimeUnit.MINUTES);
return productVO;
}
}
2. 缓存击穿:热点数据过期,所有请求打数据库
比如某个热门商品(比如秒杀商品)的缓存过期了,这时候每秒几万请求过来,都发现缓存没了,一起冲去数据库查 —— 数据库直接被打崩。
解决方案:分布式互斥锁 + 热点数据永不过期
- 分布式互斥锁:只有一个线程能去查数据库,其他线程等着,查到后更新缓存,其他线程再从缓存拿数据。
- 热点数据永不过期:对特别热门的数据,不设过期时间,而是用定时任务主动更新缓存(比如每 5 分钟更一次)。
代码实现(用 Redisson 分布式锁):
public ProductVO getHotProductDetail(Long productId) {
String cacheKey = "product:hot:detail:" + productId;
String lockKey = "lock:product:hot:" + productId;
// 1. 先查本地缓存(Caffeine):热点数据优先读本地
ProductVO localCache = caffeineCache.getIfPresent(cacheKey);
if (localCache != null) {
return localCache;
}
// 2. 查Redis缓存
String redisValue = stringRedisTemplate.opsForValue().get(cacheKey);
if (redisValue != null && !"null".equals(redisValue)) {
ProductVO productVO = JSON.parseObject(redisValue, ProductVO.class);
// 回写本地缓存
caffeineCache.put(cacheKey, productVO);
return productVO;
}
// 3. 加分布式锁:只有一个线程能查数据库
RLock lock = redissonClient.getLock(lockKey);
try {
// 加锁:30秒自动释放(避免死锁),最多等5秒
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (locked) {
// 4. 加锁成功,查数据库
ProductDO productDO = productMapper.selectById(productId);
if (productDO == null) {
stringRedisTemplate.opsForValue().set(cacheKey, "null", 1, TimeUnit.MINUTES);
returnnull;
}
// 5. 数据库有数据,更新Redis和本地缓存
ProductVO productVO = convertToVO(productDO);
// Redis不设过期时间(永不过期),靠定时任务更新
stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(productVO));
// 本地缓存设5分钟过期
caffeineCache.put(cacheKey, productVO, 5, TimeUnit.MINUTES);
return productVO;
} else {
// 加锁失败,等100ms再重试(递归或循环都行)
Thread.sleep(100);
return getHotProductDetail(productId);
}
} catch (InterruptedException e) {
log.error("获取锁异常", e);
returnnull;
} finally {
// 释放锁:只有持有锁的线程才能释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 定时任务更新热点商品缓存(每5分钟执行一次)
@Scheduled(fixedRate = 5 * 60 * 1000)
public void refreshHotProductCache() {
List<Long> hotProductIds = productMapper.selectHotProductIds(); // 查热门商品ID列表
for (Long productId : hotProductIds) {
String cacheKey = "product:hot:detail:" + productId;
ProductDO productDO = productMapper.selectById(productId);
if (productDO != null) {
ProductVO productVO = convertToVO(productDO);
// 更新Redis缓存
stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(productVO));
// 更新本地缓存
caffeineCache.put(cacheKey, productVO, 5, TimeUnit.MINUTES);
}
}
}
3. 缓存雪崩:大量 key 同时过期,请求全打数据库
比如你给所有商品缓存都设了 “凌晨 3 点过期”,到点后所有商品缓存一起失效,请求全冲去数据库 —— 这就是雪崩,比击穿更严重。
解决方案:过期时间随机 + Redis 集群 + 服务熔断降级
- 过期时间随机:给每个 key 的过期时间加个随机值(比如 30 分钟 ±5 分钟),避免同时过期。
- Redis 集群:别用单机 Redis,搞个主从 + 哨兵或者 Redis Cluster,就算一台崩了,其他的还能扛。
- 服务熔断降级:用 Sentinel 或者 Resilience4j,当数据库压力太大时,直接返回缓存的旧数据或者提示 “服务繁忙”,别硬扛。
代码实现(过期时间随机):
// 给缓存加随机过期时间:30分钟±5分钟(25-35分钟)
int baseExpire = 30; // 基础过期时间(分钟)
int randomExpire = new Random().nextInt(10); // 0-10分钟随机值
int totalExpire = baseExpire - 5 + randomExpire; // 25-35分钟
// 存Redis时用这个总过期时间
stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(productVO), totalExpire, TimeUnit.MINUTES);
服务熔断降级(用 Resilience4j):
加依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>2.1.0</version>
</dependency>
配置文件:
resilience4j:
circuitbreaker:
instances:
productDbCircuit: # 熔断器名字
slidingWindowSize: 100 # 滑动窗口大小
failureRateThreshold: 50 # 失败率阈值:50%失败就熔断
waitDurationInOpenState: 60000 # 熔断后60秒尝试恢复
permittedNumberOfCallsInHalfOpenState: 10 # 半开状态允许10个请求测试
fallback:
instances:
productDbCircuit:
fallback-exception: # 哪些异常触发降级
- java.sql.SQLException
- org.springframework.dao.DataAccessException
Service 层用注解:
// 查数据库时加熔断降级:失败了就返回缓存的旧数据(如果有的话)
@CircuitBreaker(name = "productDbCircuit", fallbackMethod = "getProductDetailFallback")
private ProductDO getProductFromDb(Long productId) {
return productMapper.selectById(productId);
}
// 降级方法:参数和返回值要和原方法一致
private ProductDO getProductDetailFallback(Long productId, Exception e) {
log.error("查数据库失败,触发降级", e);
// 尝试从Redis拿旧数据(就算过期了也拿)
String cacheKey = "product:detail:" + productId;
String redisValue = stringRedisTemplate.opsForValue().get(cacheKey);
if (redisValue != null && !"null".equals(redisValue)) {
ProductVO productVO = JSON.parseObject(redisValue, ProductVO.class);
return convertToDO(productVO); // 转成DO返回
}
// 没旧数据就抛异常(或者返回默认值)
thrownew RuntimeException("服务繁忙,请稍后再试");
}
三、多级缓存协同:怎么让三级缓存 “配合默契”
光搭好每一级还不够,得让它们 “协同工作”—— 什么时候读哪一级,什么时候更哪一级,不然会出现 “数据不一致” 的问题(比如数据库改了,缓存还是旧的)。
1. 查询流程:从外到内,层层过滤
前面给过流程图,这里再细化下,加个实际例子(查商品详情):
- 用户请求/api/v1/product/123,先到 Nginx 网关 ——Nginx 查自己的缓存,发现没有(因为商品详情是动态的,一般不存 Nginx),转发到微服务。
- 微服务实例收到请求,先查本地缓存 Caffeine(key=productDetailCache:123)—— 如果有,直接返回,全程 20ms 以内。
- 本地缓存没有,查 Redis(key=product:detail:123)—— 如果有,返回给用户,同时把数据回写到本地缓存(下次再查就快了)。
- Redis 也没有,查数据库 —— 查到后,先更 Redis,再更本地缓存,最后返回给用户。
整个流程下来,大部分请求会被本地缓存和 Redis 挡住,数据库压力很小。
2. 更新策略:两种方案,按需选择
当数据更新时(比如商品价格改了),怎么同步缓存?主要两种方案:
方案 1:失效模式(推荐)—— 更新数据库后,删除缓存
流程:更新数据库 → 删除 Redis 缓存 → 发送消息通知所有微服务实例删除本地缓存。
优点:安全,避免更新缓存失败导致不一致。
缺点:删除缓存后,第一次请求会查数据库(但有互斥锁挡着,问题不大)。
代码实现(用 RabbitMQ 通知本地缓存):
更新商品价格的 Service:
@Service
publicclass ProductUpdateService {
@Resource
private ProductMapper productMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RabbitTemplate rabbitTemplate;
@Transactional// 加事务,确保数据库更新成功才删缓存
public void updateProductPrice(Long productId, BigDecimal newPrice) {
// 1. 更新数据库
ProductDO productDO = new ProductDO();
productDO.setId(productId);
productDO.setPrice(newPrice);
productMapper.updateById(productDO);
// 2. 删除Redis缓存
String redisKey = "product:detail:" + productId;
stringRedisTemplate.delete(redisKey);
// 3. 发送消息,通知所有实例删除本地缓存
rabbitTemplate.convertAndSend("cache-invalidate-exchange", "product.detail", productId);
}
}
微服务实例接收消息,删除本地缓存:
@Component
publicclass CacheInvalidateConsumer {
@Resource
private CacheManager cacheManager;
@RabbitListener(queues = "product-detail-cache-queue")
public void handleCacheInvalidate(Long productId) {
// 获取本地缓存,删除对应的key
Cache productCache = cacheManager.getCache("productDetailCache");
if (productCache != null) {
productCache.evict(productId);
}
log.info("删除本地缓存:productDetailCache:{}", productId);
}
}
方案 2:更新模式 —— 更新数据库后,直接更新缓存
流程:更新数据库 → 更新 Redis 缓存 → 通知更新本地缓存。
优点:更新后缓存是最新的,第一次请求不用查数据库。
缺点:有并发问题,比如两个线程同时更新,可能导致缓存存旧数据。
比如:线程 A 更新数据库(价格 100)→ 线程 B 更新数据库(价格 200)→ 线程 B 更新缓存(200)→ 线程 A 更新缓存(100)—— 最后缓存是 100,数据库是 200,不一致了。
所以一般推荐用 “失效模式”,虽然第一次请求会查数据库,但更安全。
3. 数据一致性:终极解决方案(Canal)
如果你的项目对数据一致性要求特别高(比如金融场景),光靠删除缓存还不够 —— 比如更新数据库成功了,但删除 Redis 缓存失败了,这时候缓存还是旧的。
这时候可以用Canal—— 它能监听 MySQL 的 binlog 日志,当数据库数据变化时,自动同步更新缓存。
简单流程:
- Canal 伪装成 MySQL 的从库,监听 binlog 日志。
- 当 product 表的数据更新时,Canal 捕获到这个变化。
- Canal 发送消息给微服务,微服务收到后,更新 Redis 和本地缓存。
这样就算手动删除缓存失败,Canal 也能兜底,确保缓存和数据库一致。
四、实战案例:从 “卡成狗” 到 “飞起来” 的优化过程
最后给个真实案例,让大家看看多级缓存的效果 —— 去年帮一个电商客户做的商品详情接口优化,数据说话最有说服力。
优化前:只有 Redis 缓存
- 接口响应时间:80-120ms
- QPS 峰值:5000(再高就超时)
- 数据库 CPU:高峰期 60%-70%
- 问题:秒杀活动时,Redis 扛不住,接口超时率 20%+
优化后:Nginx+Caffeine+Redis 三级缓存
- 接口响应时间:15-30ms(降了 70%+)
- QPS 峰值:10 万(翻了 20 倍)
- 数据库 CPU:高峰期 5%-10%(降了 90%)
- 效果:秒杀活动时,接口零超时,数据库压力几乎可以忽略。
关键优化点:
- 首页 Banner 图、分类列表这些静态数据,用 Nginx 缓存,QPS 直接扛住 5 万。
- 热门商品详情用 Caffeine 本地缓存,90% 的请求直接在本地返回,不用查 Redis。
- Redis 只存非热门商品和兜底数据,压力大减,再配合集群,稳如老狗。
- 用 Canal 同步缓存,数据一致性没问题,客服再也没收到 “价格不一致” 的投诉。
五、踩坑指南:这些坑我替你踩过了
- 本地缓存没设最大数量:之前有个同学用 Caffeine 没设 maximumSize,上线后 JVM 内存一天天涨,最后 OOM 崩溃 —— 记住,本地缓存一定要设最大数量!
- Redis 缓存 key 没加前缀:不同业务的 key 混在一起,比如product:123和order:123,后面清理缓存时容易删错 ——key 一定要加业务前缀,比如product:detail:123。
- 分布式锁没设过期时间:加锁后如果服务崩了,锁没释放,其他线程永远拿不到锁 —— 锁一定要设自动过期时间,比如 30 秒。
- 缓存命中率没监控:不知道缓存效果怎么样,瞎调参数 —— 一定要监控 Caffeine 和 Redis 的命中率,Caffeine 命中率低于 80% 就调 maximumSize,Redis 低于 70% 就调过期时间。
- 热点数据没单独处理:把热门商品和普通商品放一个缓存,热门商品过期时导致雪崩 —— 热门商品要单独设缓存规则,用永不过期 + 定时更新。
六、总结:多级缓存不是 “银弹”,但真能救命
最后跟大家说句实在的:多级缓存不是万能的,但在微服务性能优化里,它是性价比最高的方案 —— 不用改太多代码,就能让性能翻好几倍。
记住几个核心原则:
- 分层过滤:网关拦静态,本地拦高频,Redis 拦中频,数据库扛低频。
- 按需选择:小项目本地 + Redis 就够了,大项目再补网关和 Canal。
- 先稳后快:先解决缓存穿透、击穿、雪崩这些问题,再追求性能极致。
- 监控为王:缓存命中率、Redis 内存、数据库压力,这些指标一定要监控,不然出问题都不知道在哪。
下次再遇到微服务性能不行,别光顾着加机器,先试试多级缓存,说不定几行代码就能解决问题,还能省不少服务器钱。
AI大模型学习福利
作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量
586

被折叠的 条评论
为什么被折叠?



