本地緩存的簡單實現方案有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)。
?