搭建Caffeine+Redis多级缓存机制
本地缓存的简单实现方案有HashMap,CucurrentHashMap,成熟的本地缓存方案有Guava 与 Caffeine ,企业级应用推荐下面说下两者的区别
1. 核心异同对比
特性 | Guava Cache | Caffeine |
---|---|---|
诞生背景 | Google Guava 库的一部分(2011年) | 基于 Guava Cache 重构的现代缓存库(2015+) |
性能 | 中等(锁竞争较多) | 极高(优化并发设计,吞吐量提升5~10倍) |
内存管理 | 基于 LRU 算法 | 结合 W-TinyLFU 算法(高命中率) |
过期策略 | 支持 expireAfterWrite/access | 支持 expireAfterWrite/access + refresh |
缓存回收 | 同步阻塞 | 异步非阻塞(后台线程) |
监控统计 | 基础统计(命中率等) | 详细统计(命中率、加载时间等) |
依赖 | 需引入整个 Guava 库 | 轻量(仅依赖 Caffeine) |
社区维护 | 维护模式(新功能少) | 活跃更新(Java 17+ 兼容) |
从上面的比较可知, Caffeine 各方面是优于Guava的,因此在搭建多级缓存机制时,建议使用Caffeine+Redis的组合方案。
业务执行流程:
-
请求优先读取 Caffeine 本地缓存(超快,减少网络IO)。
-
本地缓存未命中 → 读取 Redis 分布式缓存。
-
Redis 未命中 → 查询数据库,并回填到两级缓存。
下面介绍下实现方式
注意:下面的实现方式是基于Springboot 2.4+,版本不同,配置上会略有差异
1.maven中引入下面的依赖
<!-- Caffeine 本地缓存 -->
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency><!-- 缓存抽象层 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency><!-- redis 缓存操作 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions>
</dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
2.application中进行配置
spring: cache:caffeine:spec: maximumSize=1000,expireAfterWrite=10m # 本地缓存redis:time-to-live: 1h # Redis缓存过期时间# redis 配置redis:# 地址host: 127.0.0.1# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password: abc123# 连接超时时间timeout: 6000ms # 连接超时时长(毫秒)jedis:pool:max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)max-idle: 10 # 连接池中的最大空闲连接min-idle: 5 # 连接池中的最小空闲连接
3.自定义多级缓存管理器
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.github.benmanes.caffeine.cache.Caffeine;import java.util.concurrent.TimeUnit;@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {@Beanpublic RedisCacheConfiguration cacheConfiguration(CacheProperties cacheProperties) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));CacheProperties.Redis redisProperties = cacheProperties.getRedis();if (redisProperties.getTimeToLive() != null) {config = config.entryTtl(redisProperties.getTimeToLive());}if (redisProperties.getKeyPrefix() != null) {config = config.prefixKeysWith(redisProperties.getKeyPrefix());}if (!redisProperties.isCacheNullValues()) {config = config.disableCachingNullValues();}if (!redisProperties.isUseKeyPrefix()) {config = config.disableKeyPrefix();}return config;}@Beanpublic Caffeine<Object, Object> caffeineConfig() {return Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES);}@Bean@Primary // 添加 @Primary 注解指定 CaffeineCacheManager 作为默认的缓存管理器public CacheManager caffeineCacheManager(Caffeine<Object, Object> caffeine) {CaffeineCacheManager manager = new CaffeineCacheManager();manager.setCaffeine(caffeine);return manager;}@Beanpublic RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,RedisCacheConfiguration cacheConfiguration) {return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfiguration).build();}@Beanpublic CacheManager compositeCacheManager(@Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager,@Qualifier("redisCacheManager") CacheManager redisCacheManager) {return new CompositeCacheManager(caffeineCacheManager,redisCacheManager);}
}
4.业务逻辑层调用
使用示例:
@Service
public class ProductService {@Autowiredprivate ProductRepository repository;// 优先读本地缓存,其次Redis,最后数据库@Cacheable(cacheNames = "product", key = "#id")public Product getProductById(Long id) {return repository.findById(id).orElseThrow();}// 更新数据时清除两级缓存@CacheEvict(cacheNames = "product", key = "#product.id")public Product updateProduct(Product product) {return repository.save(product);}
}
手动控制多级缓存
@Service
public class CacheService {@Autowiredprivate CacheManager cacheManager;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public Product getProductWithManualControl(Long id) {// 1. 先查本地缓存Cache caffeineCache = cacheManager.getCache("product");Product product = caffeineCache.get(id, Product.class);if (product != null) {return product;}// 2. 查Redis缓存product = (Product) redisTemplate.opsForValue().get("product:" + id);if (product != null) {// 回填本地缓存caffeineCache.put(id, product);return product;}// 3. 查数据库product = repository.findById(id).orElseThrow();// 回填两级缓存redisTemplate.opsForValue().set("product:" + id, product, Duration.ofHours(1));caffeineCache.put(id, product);return product;}
}
-
缓存一致性
-
使用
@CacheEvict
或Redis Pub/Sub
同步失效两级缓存。 -
示例:通过 Redis 消息通知其他节点清理本地缓存。
-
-
防止缓存击穿
-
Caffeine 配置
refreshAfterWrite
:
-
Caffeine.newBuilder().refreshAfterWrite(5, TimeUnit.MINUTES).build(key -> loadFromRedisOrDb(key));
3.监控统计:
Caffeine 统计:cache.getNativeCache().stats()
Redis 统计:INFO commandstats
4. 验证多级缓存
本地缓存生效:连续调用同一接口,观察第二次响应时间骤降。
Redis 缓存生效:重启应用后,首次请求仍快速返回(数据来自Redis)。