多级缓存
2026/1/15大约 4 分钟SpringSpring CacheJava缓存后端
多级缓存
为什么需要多级缓存
单一缓存存在局限性:
| 缓存类型 | 优点 | 缺点 |
|---|---|---|
| 本地缓存 | 速度快、无网络开销 | 容量有限、集群不一致 |
| 分布式缓存 | 容量大、集群共享 | 网络开销、延迟较高 |
多级缓存结合两者优势:
实现方案
方案一:自定义 CacheManager
@Configuration
@EnableCaching
public class MultiLevelCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 本地缓存 - Caffeine
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES));
// 分布式缓存 - Redis
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)))
.build();
// 组合缓存管理器
return new MultiLevelCacheManager(caffeineCacheManager, redisCacheManager);
}
}自定义多级缓存管理器
public class MultiLevelCacheManager implements CacheManager {
private final CacheManager localCacheManager;
private final CacheManager remoteCacheManager;
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
public MultiLevelCacheManager(CacheManager local, CacheManager remote) {
this.localCacheManager = local;
this.remoteCacheManager = remote;
}
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name, n -> {
Cache localCache = localCacheManager.getCache(n);
Cache remoteCache = remoteCacheManager.getCache(n);
return new MultiLevelCache(n, localCache, remoteCache);
});
}
@Override
public Collection<String> getCacheNames() {
Set<String> names = new HashSet<>();
names.addAll(localCacheManager.getCacheNames());
names.addAll(remoteCacheManager.getCacheNames());
return names;
}
}自定义多级缓存
public class MultiLevelCache implements Cache {
private final String name;
private final Cache localCache;
private final Cache remoteCache;
public MultiLevelCache(String name, Cache local, Cache remote) {
this.name = name;
this.localCache = local;
this.remoteCache = remote;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(Object key) {
// 先查本地缓存
ValueWrapper value = localCache.get(key);
if (value != null) {
return value;
}
// 再查远程缓存
value = remoteCache.get(key);
if (value != null) {
// 回填本地缓存
localCache.put(key, value.get());
}
return value;
}
@Override
public void put(Object key, Object value) {
localCache.put(key, value);
remoteCache.put(key, value);
}
@Override
public void evict(Object key) {
localCache.evict(key);
remoteCache.evict(key);
}
@Override
public void clear() {
localCache.clear();
remoteCache.clear();
}
}方案二:使用 Caffeine + Redis
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, User> redisTemplate;
private final Cache<Long, User> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public User getById(Long id) {
// L1: 本地缓存
User user = localCache.getIfPresent(id);
if (user != null) {
return user;
}
// L2: Redis
String key = "user:" + id;
user = redisTemplate.opsForValue().get(key);
if (user != null) {
localCache.put(id, user);
return user;
}
// L3: 数据库
user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
localCache.put(id, user);
}
return user;
}
}缓存一致性
多级缓存面临的最大挑战是缓存一致性。
问题场景
解决方案:Redis Pub/Sub
@Configuration
public class CacheMessageConfig {
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
CacheMessageListener listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listener, new ChannelTopic("cache:evict"));
return container;
}
}
@Component
public class CacheMessageListener implements MessageListener {
@Autowired
private CaffeineCacheManager localCacheManager;
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
CacheEvictMessage msg = JSON.parseObject(body, CacheEvictMessage.class);
// 清除本地缓存
Cache cache = localCacheManager.getCache(msg.getCacheName());
if (cache != null) {
if (msg.getKey() != null) {
cache.evict(msg.getKey());
} else {
cache.clear();
}
}
}
}
@Data
public class CacheEvictMessage {
private String cacheName;
private Object key;
}发布缓存失效消息
@Service
public class CacheEvictService {
@Autowired
private StringRedisTemplate redisTemplate;
public void evict(String cacheName, Object key) {
CacheEvictMessage message = new CacheEvictMessage();
message.setCacheName(cacheName);
message.setKey(key);
redisTemplate.convertAndSend("cache:evict", JSON.toJSONString(message));
}
}
// 使用 AOP 自动发布
@Aspect
@Component
public class CacheEvictAspect {
@Autowired
private CacheEvictService cacheEvictService;
@AfterReturning("@annotation(cacheEvict)")
public void afterEvict(JoinPoint point, CacheEvict cacheEvict) {
// 解析 Key 并发布消息
for (String cacheName : cacheEvict.value()) {
cacheEvictService.evict(cacheName, resolveKey(point, cacheEvict));
}
}
}最佳实践
1. 合理设置过期时间
// 本地缓存过期时间 < 远程缓存过期时间
Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES) // 本地 5 分钟
.build();
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)); // Redis 30 分钟2. 热点数据预加载
@Component
public class CacheWarmer implements ApplicationRunner {
@Autowired
private UserService userService;
@Override
public void run(ApplicationArguments args) {
// 预加载热点数据
List<Long> hotUserIds = getHotUserIds();
hotUserIds.forEach(userService::getById);
}
}3. 缓存穿透防护
public User getById(Long id) {
User user = localCache.getIfPresent(id);
if (user != null) {
return user == NULL_USER ? null : user;
}
user = loadFromRedisOrDb(id);
// 缓存空对象防止穿透
localCache.put(id, user != null ? user : NULL_USER);
return user;
}
private static final User NULL_USER = new User();小结
多级缓存通过组合本地缓存和分布式缓存,兼顾了访问速度和数据一致性。实现时需要注意缓存一致性问题,可以通过 Redis Pub/Sub 等机制同步缓存失效消息。
面试题预览
常见面试题
- 为什么需要多级缓存?
- 多级缓存如何保证一致性?
- 本地缓存和分布式缓存的过期时间如何设置?
