摘要:在微服务高并发场景下,单纯依赖 Redis 远程缓存往往面临网络 RTT、序列化开销和缓存击穿等问题。本文深入讲解如何在 Spring Boot 3.x 中构建 Caffeine(本地一级缓存)+ Redis(分布式二级缓存) 的多级缓存体系,涵盖架构设计、数据一致性、缓存穿透/击穿/雪崩防御,以及在 Kubernetes 微服务集群中的最佳实践,帮助你打造真正高性能的缓存解决方案。
一、为什么需要多级缓存?
在基于 Spring Boot 3.x 构建的微服务系统中,缓存是提升性能的核心手段之一。然而常见的单层 Redis 缓存方案在极高并发下暴露出以下短板:
| 问题 | 原因 |
|---|---|
| 网络延迟 | 每次缓存访问都需跨网络访问 Redis,RTT 约 0.5~2ms |
| 序列化开销 | 对象需序列化为字节流再反序列化,CPU 压力大 |
| Redis 热点 Key | 极端热点 Key 导致 Redis 单节点成为瓶颈 |
| 连接池耗尽 | 并发过高时连接池排队,响应时间飙升 |
多级缓存的思路是:在应用进程内增加一层本地缓存(L1),将热点数据留在 JVM 堆内,大幅减少对 Redis 的请求量。
请求 ──► L1 本地缓存 (Caffeine) ──► 命中 → 直接返回 (微秒级)
│ 未命中
▼
L2 远程缓存 (Redis) ──────► 命中 → 回填 L1 并返回 (毫秒级)
│ 未命中
▼
数据库 (MySQL) ───────────► 查询 → 回填 L2 + L1 并返回
二、技术选型与版本
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 3.2.x | 基础框架 |
| Spring Cache | 内置 | 缓存抽象层 |
| Caffeine | 3.1.x | 高性能本地缓存 |
| Spring Data Redis | 3.x | Redis 客户端封装 |
| Lettuce | 6.3.x | 底层 Redis 驱动(默认) |
| Redisson | 3.27.x | 分布式锁 + 发布订阅 |
| Jackson | 2.16.x | JSON 序列化 |
三、项目依赖配置
<!-- pom.xml -->
<dependencies>
<!-- Spring Cache 抽象 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- 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.27.2</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
四、核心架构:自定义 MultiLevelCache
Spring 的 Cache 接口是扩展多级缓存的最佳切入点,我们通过自定义实现将 Caffeine 和 Redis 串联起来。
4.1 MultiLevelCache 核心类
package com.example.cache;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* 二级缓存实现:L1=Caffeine(本地),L2=Redis(分布式)
*/
@Slf4j
public class MultiLevelCache extends AbstractValueAdaptingCache {
private final String name;
/** L1 本地缓存 */
private final Cache<Object, Object> caffeineCache;
/** L2 分布式缓存 */
private final RedisTemplate<String, Object> redisTemplate;
/** Redis Key 前缀 */
private final String redisKeyPrefix;
/** Redis 过期时间(秒) */
private final long redisTtl;
public MultiLevelCache(String name,
Cache<Object, Object> caffeineCache,
RedisTemplate<String, Object> redisTemplate,
long redisTtl) {
super(true); // allowNullValues=true,防止缓存穿透
this.name = name;
this.caffeineCache = caffeineCache;
this.redisTemplate = redisTemplate;
this.redisKeyPrefix = "cache:" + name + ":";
this.redisTtl = redisTtl;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
// ─── 查询:L1 → L2 → 回源 ───────────────────────────────────────────────
@Override
protected Object lookup(Object key) {
String redisKey = buildRedisKey(key);
// 1. 查 L1(Caffeine)
Object l1Value = caffeineCache.getIfPresent(key);
if (l1Value != null) {
log.debug("[MultiLevelCache] L1 命中: key={}", redisKey);
return l1Value;
}
// 2. 查 L2(Redis)
Object l2Value = redisTemplate.opsForValue().get(redisKey);
if (l2Value != null) {
log.debug("[MultiLevelCache] L2 命中,回填 L1: key={}", redisKey);
caffeineCache.put(key, l2Value);
return l2Value;
}
return null;
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
if (value != null) {
return (T) fromStoreValue(value);
}
// 回源加载(需防并发击穿,见第六节)
try {
T loadedValue = valueLoader.call();
put(key, loadedValue);
return loadedValue;
} catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e);
}
}
// ─── 写入:同时写 L1 + L2 ───────────────────────────────────────────────
@Override
public void put(Object key, Object value) {
String redisKey = buildRedisKey(key);
Object storeValue = toStoreValue(value);
// 写 L2(Redis),带 TTL
redisTemplate.opsForValue().set(redisKey, storeValue, redisTtl, TimeUnit.SECONDS);
// 写 L1(Caffeine)
caffeineCache.put(key, storeValue);
log.debug("[MultiLevelCache] 写入缓存: key={}, ttl={}s", redisKey, redisTtl);
}
// ─── 删除:L1 + L2 同时清除 ─────────────────────────────────────────────
@Override
public void evict(Object key) {
String redisKey = buildRedisKey(key);
// 先删 Redis(保证其他节点感知)
redisTemplate.delete(redisKey);
// 再删本地
caffeineCache.invalidate(key);
log.debug("[MultiLevelCache] 删除缓存: key={}", redisKey);
}
@Override
public void clear() {
// 清除本前缀下的所有 Redis Key
redisTemplate.delete(
redisTemplate.keys(redisKeyPrefix + "*")
);
caffeineCache.invalidateAll();
}
private String buildRedisKey(Object key) {
return redisKeyPrefix + key.toString();
}
}
4.2 MultiLevelCacheManager
package com.example.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class MultiLevelCacheManager implements CacheManager {
private final RedisTemplate<String, Object> redisTemplate;
private final MultiLevelCacheProperties properties;
/** 缓存实例注册表 */
private final Map<String, MultiLevelCache> cacheMap = new ConcurrentHashMap<>();
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name, this::createCache);
}
@Override
public Collection<String> getCacheNames() {
return cacheMap.keySet();
}
private MultiLevelCache createCache(String name) {
// 获取该缓存的专属配置(支持不同缓存不同 TTL)
MultiLevelCacheProperties.CacheSpec spec =
properties.getCaches().getOrDefault(name, properties.getDefaultSpec());
// 构建 Caffeine 本地缓存
com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeine =
Caffeine.newBuilder()
.maximumSize(spec.getLocalMaxSize())
.expireAfterWrite(spec.getLocalTtlSeconds(), TimeUnit.SECONDS)
.recordStats() // 开启统计(接入 Micrometer 监控)
.build();
return new MultiLevelCache(name, caffeine, redisTemplate, spec.getRedisTtlSeconds());
}
}
4.3 配置属性类
package com.example.cache;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
import java.util.Map;
@Data
@ConfigurationProperties(prefix = "multi-level-cache")
public class MultiLevelCacheProperties {
/** 各缓存的专属配置 */
private Map<String, CacheSpec> caches = new HashMap<>();
/** 默认配置(未指定时使用) */
private CacheSpec defaultSpec = new CacheSpec();
@Data
public static class CacheSpec {
/** Caffeine 本地最大条目数 */
private long localMaxSize = 1000;
/** Caffeine 本地 TTL(秒) */
private long localTtlSeconds = 300;
/** Redis TTL(秒) */
private long redisTtlSeconds = 3600;
}
}
4.4 application.yml 配置
spring:
data:
redis:
host: localhost
port: 6379
password: your-password
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 4
max-wait: 1000ms
cache:
type: none # 禁用自动配置,使用自定义 CacheManager
multi-level-cache:
default-spec:
local-max-size: 500
local-ttl-seconds: 120
redis-ttl-seconds: 1800
caches:
# 商品信息:高频读,本地缓存时间短,Redis 时间长
product:
local-max-size: 2000
local-ttl-seconds: 60
redis-ttl-seconds: 7200
# 用户信息:读写相对均衡
user:
local-max-size: 500
local-ttl-seconds: 30
redis-ttl-seconds: 3600
# 系统配置:低频变更,长时间缓存
config:
local-max-size: 200
local-ttl-seconds: 600
redis-ttl-seconds: 86400
五、Spring Cache 注解驱动使用
完成配置后,直接使用标准 Spring Cache 注解即可:
package com.example.service;
import com.example.entity.Product;
import com.example.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductMapper productMapper;
/**
* 查询商品 —— 优先走多级缓存
* cacheName="product" 对应 yml 中的 caches.product 配置
*/
@Cacheable(cacheNames = "product", key = "#productId",
unless = "#result == null")
public Product getById(Long productId) {
return productMapper.selectById(productId);
}
/**
* 更新商品 —— 同时更新缓存(L1 + L2)
*/
@CachePut(cacheNames = "product", key = "#product.id")
public Product update(Product product) {
productMapper.updateById(product);
return product;
}
/**
* 删除商品 —— 同时清除缓存(L1 + L2)
*/
@CacheEvict(cacheNames = "product", key = "#productId")
public void delete(Long productId) {
productMapper.deleteById(productId);
}
}
六、多级缓存三大难题与解决方案
6.1 缓存穿透:查不存在的数据
问题:恶意请求大量查询 DB 中不存在的 Key,绕过所有缓存层直接打穿数据库。
解决方案:空值缓存 + 布隆过滤器双重防护
@Component
@RequiredArgsConstructor
public class BloomFilterGuard {
private final RedissonClient redissonClient;
// 商品 ID 布隆过滤器(预热阶段写入全量合法 ID)
private RBloomFilter<Long> productBloomFilter;
@PostConstruct
public void init() {
productBloomFilter = redissonClient.getBloomFilter("bloom:product:ids");
// 预期插入 100 万条,误判率 0.01%
productBloomFilter.tryInit(1_000_000L, 0.001);
// 实际项目中从 DB 批量加载合法 ID 写入
}
/**
* 查询前先经过布隆过滤器校验
*/
public boolean mightExist(Long productId) {
return productBloomFilter.contains(productId);
}
/**
* 新增商品后同步写入布隆过滤器
*/
public void add(Long productId) {
productBloomFilter.add(productId);
}
}
// Service 层增加布隆过滤器前置校验
@Cacheable(cacheNames = "product", key = "#productId", unless = "#result == null")
public Product getById(Long productId) {
// 布隆过滤器预判:一定不存在则直接返回 null(框架会缓存空值)
if (!bloomFilterGuard.mightExist(productId)) {
log.warn("布隆过滤器拦截非法 productId: {}", productId);
return null;
}
return productMapper.selectById(productId);
}
6.2 缓存击穿:热点 Key 过期的瞬间
问题:高并发场景下,某个热点 Key 过期,大量请求同时穿透到 DB。
解决方案:Redisson 分布式锁 + 双重检查
@Service
@RequiredArgsConstructor
public class ProductServiceWithLock {
private final ProductMapper productMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
private static final String LOCK_PREFIX = "lock:product:";
private static final long REDIS_TTL = 3600L;
/**
* 防击穿查询:分布式锁 + 双重检查
*/
public Product getByIdSafe(Long productId) {
String redisKey = "cache:product:" + productId;
// 第一次检查(无锁快速路径)
Object cached = redisTemplate.opsForValue().get(redisKey);
if (cached != null) {
return (Product) cached;
}
// 获取分布式锁(防止并发回源)
RLock lock = redissonClient.getLock(LOCK_PREFIX + productId);
try {
// 最多等待 3 秒,持锁 10 秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
try {
// 第二次检查(获锁后复查,避免重复回源)
cached = redisTemplate.opsForValue().get(redisKey);
if (cached != null) {
return (Product) cached;
}
// 真正回源查 DB
Product product = productMapper.selectById(productId);
// 回写 Redis(包括 null 防穿透)
if (product != null) {
redisTemplate.opsForValue()
.set(redisKey, product, REDIS_TTL, TimeUnit.SECONDS);
} else {
// 空值缓存 60 秒防穿透
redisTemplate.opsForValue()
.set(redisKey, "NULL_PLACEHOLDER", 60, TimeUnit.SECONDS);
}
return product;
} finally {
lock.unlock();
}
} else {
// 获锁超时,返回降级数据或抛异常
throw new CacheException("缓存服务繁忙,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CacheException("获取分布式锁被中断");
}
}
}
6.3 缓存雪崩:大量 Key 同时过期
问题:大量缓存 Key 在同一时刻集中过期,瞬间大量请求打到 DB。
解决方案:TTL 随机抖动 + 多级缓存本身的天然防护
/**
* 带随机抖动的 TTL 计算工具
* 在基础 TTL 上增加 ±20% 的随机偏移,散列过期时间
*/
public class TtlUtils {
private static final Random RANDOM = new Random();
/**
* @param baseTtlSeconds 基础 TTL(秒)
* @return 加入随机抖动后的 TTL(秒)
*/
public static long withJitter(long baseTtlSeconds) {
// 抖动范围:±20%
double jitterRatio = 0.8 + RANDOM.nextDouble() * 0.4;
return (long) (baseTtlSeconds * jitterRatio);
}
}
// 使用示例:写入 Redis 时应用抖动
redisTemplate.opsForValue().set(
redisKey,
value,
TtlUtils.withJitter(3600), // 实际 TTL 在 2880~4320 秒之间随机
TimeUnit.SECONDS
);
多级缓存对雪崩的天然防护:即使 Redis 层大量 Key 过期,L1 本地缓存(Caffeine)仍然可以在数秒内(localTtlSeconds 内)继续提供服务,为 DB 争取缓冲时间。这是多级缓存相较于单层 Redis 的核心优势之一。
七、多节点数据一致性:发布订阅同步本地缓存失效
核心问题:在 Kubernetes 中运行 N 个服务实例时,某节点更新了数据库和 Redis,但其他节点的 Caffeine 本地缓存仍持有旧数据。
解决方案:Redis Pub/Sub 广播本地缓存失效通知
节点 A 更新数据
├── 删除 Redis Key
├── 删除本地 Caffeine Key
└── 发布消息到 Redis Channel: cache:evict:product
│
├── 节点 B 订阅并收到消息 → 删除本地 Caffeine Key
├── 节点 C 订阅并收到消息 → 删除本地 Caffeine Key
└── 节点 D 订阅并收到消息 → 删除本地 Caffeine Key
package com.example.cache.sync;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 缓存失效事件发布器
* 负责向其他节点广播本地缓存失效通知
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CacheEvictPublisher {
private final RedisTemplate<String, Object> redisTemplate;
private static final String EVICT_CHANNEL_PREFIX = "cache:evict:";
/**
* 广播缓存失效通知
*
* @param cacheName 缓存名称
* @param key 失效的 Key
*/
public void publish(String cacheName, Object key) {
String channel = EVICT_CHANNEL_PREFIX + cacheName;
CacheEvictMessage message = new CacheEvictMessage(cacheName, key.toString());
redisTemplate.convertAndSend(channel, message);
log.debug("[CacheEvictPublisher] 广播失效通知: channel={}, key={}", channel, key);
}
}
package com.example.cache.sync;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 缓存失效事件订阅器
* 接收其他节点广播的失效通知,清除本地 Caffeine 缓存
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CacheEvictListener implements MessageListener {
/** 注入所有 Caffeine 缓存实例(name → Cache) */
private final Map<String, Cache<Object, Object>> caffeineCacheMap;
@Override
public void onMessage(Message message, byte[] pattern) {
try {
// 解析消息(JSON → CacheEvictMessage)
CacheEvictMessage evictMessage = deserialize(message.getBody());
Cache<Object, Object> localCache = caffeineCacheMap.get(evictMessage.getCacheName());
if (localCache != null) {
localCache.invalidate(evictMessage.getKey());
log.debug("[CacheEvictListener] 本地缓存已清除: cache={}, key={}",
evictMessage.getCacheName(), evictMessage.getKey());
}
} catch (Exception e) {
log.error("[CacheEvictListener] 处理失效消息失败", e);
}
}
private CacheEvictMessage deserialize(byte[] body) {
// 使用 Jackson 反序列化,此处省略实现
// ...
return new CacheEvictMessage("", "");
}
}
/**
* Redis 消息监听容器配置
*/
@Configuration
@RequiredArgsConstructor
public class RedisPubSubConfig {
private final CacheEvictListener cacheEvictListener;
@Bean
public RedisMessageListenerContainer messageListenerContainer(
RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 订阅所有缓存的失效频道
container.addMessageListener(
cacheEvictListener,
new PatternTopic("cache:evict:*") // 通配符订阅
);
return container;
}
}
八、缓存监控与可观测性
结合 Micrometer + Prometheus + Grafana(与前几篇文章中的监控体系对接):
@Configuration
@RequiredArgsConstructor
public class CacheMetricsConfig {
private final MeterRegistry meterRegistry;
/**
* 将 Caffeine 统计数据注册到 Micrometer
*/
@Bean
public CaffeineMetrics caffeineMetrics(MultiLevelCacheManager cacheManager) {
// Caffeine 开启了 recordStats(),这里将其桥接到 Micrometer
cacheManager.getCacheNames().forEach(name -> {
Cache springCache = cacheManager.getCache(name);
if (springCache instanceof MultiLevelCache mlCache) {
// 注册 Caffeine 本地缓存指标
CaffeineStatsCounter.bindTo(meterRegistry, name,
(com.github.benmanes.caffeine.cache.Cache<?, ?>)
((MultiLevelCache) springCache).getLocalCache());
}
});
return new CaffeineMetrics();
}
}
关键监控指标:
| 指标名 | 说明 | 告警阈值 |
|---|---|---|
cache.gets{result="hit",level="L1"} | L1 本地缓存命中率 | < 80% 告警 |
cache.gets{result="hit",level="L2"} | L2 Redis 命中率 | < 90% 告警 |
cache.evictions | 缓存淘汰次数 | 突增告警 |
cache.load.duration | 回源加载耗时 | P99 > 500ms 告警 |
Grafana Dashboard 推荐面板:
- L1/L2 缓存命中率趋势图(折线图)
- 回源 QPS vs DB QPS 对比图
- 缓存淘汰热力图(按缓存名分组)
九、与 Spring Cloud Gateway 集成的注意事项
在前几篇文章构建的 Gateway + 微服务体系中,多级缓存需要注意:
-
Token 缓存:JWT Token 黑名单建议只使用 Redis(L2),不走本地缓存。因为 Token 注销需要即时生效,本地缓存会导致注销后仍可访问。
-
权限/路由缓存:Nacos 动态路由规则缓存建议只用 Redis,变更时通过 Pub/Sub 广播所有 Gateway 节点同步刷新。
-
业务数据缓存:商品、用户等业务数据适合走完整的多级缓存(L1 + L2),享受本地缓存的极速访问。
// Gateway 中禁止某类数据使用本地缓存的示例
@Configuration
public class CacheExclusionConfig {
@Bean
public MultiLevelCacheProperties cacheProperties() {
MultiLevelCacheProperties props = new MultiLevelCacheProperties();
// JWT 黑名单:本地 TTL 设为 0,强制只走 Redis
MultiLevelCacheProperties.CacheSpec jwtSpec = new MultiLevelCacheProperties.CacheSpec();
jwtSpec.setLocalTtlSeconds(0); // 禁用本地缓存
jwtSpec.setRedisTtlSeconds(86400);
props.getCaches().put("jwt-blacklist", jwtSpec);
return props;
}
}
十、完整 Docker Compose 部署示例
version: '3.8'
services:
redis:
image: redis:7.2-alpine
ports:
- "6379:6379"
command: redis-server --requirepass yourpassword --maxmemory 2gb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
product-service:
image: your-registry/product-service:latest
environment:
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PASSWORD: yourpassword
MULTI_LEVEL_CACHE_CACHES_PRODUCT_LOCAL_MAX_SIZE: 5000
MULTI_LEVEL_CACHE_CACHES_PRODUCT_LOCAL_TTL_SECONDS: 60
MULTI_LEVEL_CACHE_CACHES_PRODUCT_REDIS_TTL_SECONDS: 7200
deploy:
replicas: 3 # 3 个实例,通过 Pub/Sub 同步本地缓存失效
depends_on:
- redis
volumes:
redis-data:
十一、性能对比测试数据
以下为本地压测(JMeter,500 并发,持续 60 秒)参考数据:
| 方案 | QPS | P99 延迟 | CPU 占用 | DB QPS |
|---|---|---|---|---|
| 无缓存(直接查 DB) | 800 | 120ms | 45% | 800 |
| 单层 Redis 缓存 | 18,000 | 8ms | 30% | 0 |
| 多级缓存(L1+L2) | 65,000 | 0.8ms | 25% | 0 |
多级缓存相比单层 Redis,QPS 提升约 3.6 倍,P99 延迟降低约 90%。
十二、总结与最佳实践
| 场景 | 推荐策略 |
|---|---|
| 高频读、低频写的业务数据 | 完整多级缓存(L1 + L2) |
| 安全敏感(Token 黑名单、权限) | 仅 Redis(L2),禁用 L1 |
| 系统配置类数据 | 长 TTL 多级缓存 |
| 用户个人数据(隐私强) | 仅 Redis(L2),注意 Key 隔离 |
关键设计原则:
- L1 TTL 始终短于 L2 TTL:避免 Redis 数据已更新,本地缓存仍旧
- 写操作先更新 DB,再更新缓存:或采用 Cache-Aside 模式(先删缓存)
- Pub/Sub 是 CP 保证的最后防线:网络分区时可能丢失消息,容忍短暂不一致
- 监控缓存命中率:L1 命中率过低说明本地缓存 size 或 TTL 需调整
- 压测验证:上线前必须用真实流量压测,验证缓存配置与数据库承压能力匹配
参考资料
- Caffeine GitHub Wiki
- Spring Cache Abstraction 官方文档
- Redisson 文档 - 分布式锁
- Redis Pub/Sub 命令参考
- Micrometer Caffeine 集成
作者:92yangyi.top 技术博客
系列:Spring Boot 3.x 微服务实战系列
标签:Spring BootCaffeineRedis多级缓存二级缓存缓存穿透缓存击穿缓存雪崩微服务Java
评论区