Spring Boot 两级缓存(Caffeine + Redis)完整实现(含源码)
本文详细介绍如何在 Spring Boot 2.x / 3.x
项目中实现一个高性能、可广播失效的 两级缓存机制(Caffeine +
Redis),并提供完整可运行源码,适合直接集成或二次封装。
完整代码参考gitee地址: https://gitee.com/xizhyu66/twolevelcache-lab
一、设计目标
目标 说明
性能 L1 本地缓存(Caffeine)提供纳秒级访问速度
一致性 L2 Redis 实现跨实例共享,通过 Pub/Sub 保持最终一致
兼容性 兼容 @Cacheable / @CacheEvict 等 Spring Cache 注解
扩展性 可插拔支持不同 L1/L2 实现,例如 Ehcache + Redis
二、整体架构
text
┌────────────────────────────┐
│ 应用实例 A │
│ ┌────────────┐ │
请求 → 缓存查 → │ │ L1:Caffeine │ │
│ └─────┬──────┘ │
│ │ 回填 │
│ ┌─────▼──────┐ │
│ │ L2:Redis │◄───┐ 广播 │
│ └────────────┘ │ │
└────────────────────┘ │
▲ │
│ Redis Pub/Sub 通知 │
│ ▼
┌────────────────────────────┐
│ 应用实例 B │
│ ┌────────────┐ │
│ │ L1:Caffeine │ │
│ └────────────┘ │
└────────────────────────────┘
三、核心思路
- 读缓存 :先查 L1 → miss → 查 L2 → miss → 回源 DB。若命中
L2,则回填 L1。 - 写更新 :写 DB → 删除 L2 → 通过 Redis Pub/Sub 通知其他节点清理
L1。 - 一致性策略:允许短暂不一致(最终一致),结合延迟双删保障强一致写。
四、完整源码
maven依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.4.6</version>
</dependency>
配置(application.yml)
java
spring:
cache:
type: none # 我们自定义两级Cache,不用Spring默认单个CacheManager
redis:
host: 127.0.0.1
port: 6379
caffeine:
spec: maximumSize=10000,expireAfterWrite=5m,recordStats
两级 CacheManager(示例实现要点)
java
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.afterPropertiesSet();
// L2: Redis Cache
RedisCacheManager redisMgr = RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)))
.build();
// L1: Caffeine Cache
CaffeineCacheManager caffeineMgr = new CaffeineCacheManager();
caffeineMgr.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5)));
// 组合:先查 L1,再查 L2;put 时写两边
return new TwoLevelCacheManager(caffeineMgr, redisMgr);
}
// 订阅失效广播,清 L1
@Bean
public MessageListenerAdapter invalidateListener(TwoLevelCacheManager mgr) {
return new MessageListenerAdapter((MessageListener) (msg, pattern) -> {
String key = new String(msg.getBody(), StandardCharsets.UTF_8);
mgr.invalidateLocal(key);
});
}
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory cf,
MessageListenerAdapter listener) {
RedisMessageListenerContainer c = new RedisMessageListenerContainer();
c.setConnectionFactory(cf);
c.addMessageListener(listener, new PatternTopic("cache:invalidate"));
return c;
}
}
📄 TwoLevelCacheManager.java
java
package com.example.cache;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class TwoLevelCacheManager implements CacheManager {
private final CacheManager l1Manager;
private final CacheManager l2Manager;
private final StringRedisTemplate stringRedisTemplate;
private final String invalidateTopic;
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
public TwoLevelCacheManager(CacheManager l1Manager, CacheManager l2Manager) {
this(l1Manager, l2Manager, null, null);
}
public TwoLevelCacheManager(CacheManager l1Manager,
CacheManager l2Manager,
StringRedisTemplate stringRedisTemplate,
String invalidateTopic) {
this.l1Manager = Objects.requireNonNull(l1Manager, "L1 manager required");
this.l2Manager = Objects.requireNonNull(l2Manager, "L2 manager required");
this.stringRedisTemplate = stringRedisTemplate;
this.invalidateTopic = invalidateTopic;
}
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name, n -> {
Cache l1 = l1Manager.getCache(n);
Cache l2 = l2Manager.getCache(n);
return new TwoLevelCache(n, l1, l2, this::publishInvalidate);
});
}
@Override
public Collection<String> getCacheNames() {
Set<String> names = new LinkedHashSet<>(l1Manager.getCacheNames());
names.addAll(l2Manager.getCacheNames());
return names;
}
public void invalidateLocal(String cacheName, Object key) {
Cache cache = cacheMap.get(cacheName);
if (cache instanceof TwoLevelCache) ((TwoLevelCache) cache).invalidateLocal(key);
}
public void invalidateLocal(String cacheName) {
Cache cache = cacheMap.get(cacheName);
if (cache instanceof TwoLevelCache) ((TwoLevelCache) cache).invalidateLocalAll();
}
private void publishInvalidate(String cacheName, Object keyOrWildcard) {
if (stringRedisTemplate == null || invalidateTopic == null) return;
String payload = keyOrWildcard == null ? cacheName + "::*" : cacheName + "::" + keyOrWildcard;
stringRedisTemplate.convertAndSend(invalidateTopic, payload);
}
}
📄 TwoLevelCache.java
java
package com.example.cache;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import java.util.concurrent.Callable;
class TwoLevelCache implements Cache {
private final String name;
private final Cache l1;
private final Cache l2;
private final InvalidatePublisher publisher;
@FunctionalInterface
interface InvalidatePublisher {
void publish(String cacheName, Object keyOrWildcard);
}
TwoLevelCache(String name, Cache l1, Cache l2, InvalidatePublisher publisher) {
this.name = name; this.l1 = l1; this.l2 = l2; this.publisher = publisher;
}
@Override
public String getName() { return name; }
@Override
public Object getNativeCache() { return Map.of("l1", l1.getNativeCache(), "l2", l2.getNativeCache()); }
@Override
public ValueWrapper get(Object key) {
ValueWrapper v = l1.get(key);
if (v != null) return v;
v = l2.get(key);
if (v != null) l1.put(key, v.get());
return v;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper vw = get(key);
if (vw != null) return (T) vw.get();
try {
T val = valueLoader.call();
if (val != null) { l2.put(key, val); l1.put(key, val); }
return val;
} catch (Exception e) { throw new ValueRetrievalException(key, valueLoader, e); }
}
@Override
public void put(Object key, Object value) {
l2.put(key, value);
l1.put(key, value);
publisher.publish(name, key);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
ValueWrapper existing = get(key);
if (existing != null) return existing;
put(key, value);
return new SimpleValueWrapper(value);
}
@Override
public void evict(Object key) {
l2.evict(key); l1.evict(key);
publisher.publish(name, key);
}
@Override
public void clear() {
l2.clear(); l1.clear();
publisher.publish(name, null);
}
void invalidateLocal(Object key) { l1.evict(key); }
void invalidateLocalAll() { l1.clear(); }
}
五、Redis 订阅配置
java
@Bean
public RedisMessageListenerContainer cacheInvalidateListenerContainer(
RedisConnectionFactory cf, TwoLevelCacheManager twoLevelCacheManager) {
RedisMessageListenerContainer c = new RedisMessageListenerContainer();
c.setConnectionFactory(cf);
c.addMessageListener((message, pattern) -> {
String payload = new String(message.getBody(), java.nio.charset.StandardCharsets.UTF_8);
int idx = payload.indexOf("::");
if (idx > 0) {
String cacheName = payload.substring(0, idx);
String key = payload.substring(idx + 2);
if ("*".equals(key)) twoLevelCacheManager.invalidateLocal(cacheName);
else twoLevelCacheManager.invalidateLocal(cacheName, key);
}
}, new org.springframework.data.redis.listener.ChannelTopic("cache:invalidate"));
return c;
}
六、使用示例
java
@Service
public class UserService {
@Cacheable(cacheNames = "user", key = "#id") // 读走两级缓存
public UserDTO findById(Long id) { return repo.findById(id); }
@Transactional
public void updateUser(UserDTO u) {
repo.update(u);
redisTemplate.delete("user::" + u.getId()); // 删 L2
stringRedisTemplate.convertAndSend("cache:invalidate",
"user::" + u.getId()); // 广播删 L1
// 可选:延迟双删
taskScheduler.schedule(() ->
redisTemplate.delete("user::" + u.getId()),
Instant.now().plusMillis(100));
}
}
七、缓存策略建议
问题类型 解决方案
缓存穿透 缓存空对象 + BloomFilter
缓存击穿 单 Key 互斥锁 + 逻辑过期
缓存雪崩 TTL 加随机抖动
热点 Key 独立 TTL + 预热机制
一致性 延迟双删 + Pub/Sub 广播
八、总结
本方案实现: - ✅ 高性能读写路径(Caffeine + Redis)\
- ✅ 简洁无侵入整合 Spring Cache\
- ✅ 支持多实例缓存一致性广播\
- ✅ 可自由扩展到多级缓存或多数据源缓存场景
推荐使用场景:高并发读多写少的服务,如用户资料、配置中心、商品详情、字典表等。