搭建Caffeine+Redis多級緩存機制
本地緩存的簡單實現(xiàn)方案有HashMap,CucurrentHashMap,成熟的本地緩存方案有Guava 與 Caffeine ,企業(yè)級應用推薦下面說下兩者的區(qū)別
0. 核心異同對比
| 特性 | Guava Cache | Caffeine |
|---|---|---|
| 誕生背景 | Google Guava 庫的一部分(2011年) | 基于 Guava Cache 重構(gòu)的現(xiàn)代緩存庫(2015+) |
| 性能 | 中等(鎖競爭較多) | 極高(優(yōu)化并發(fā)設(shè)計,吞吐量提升5~10倍) |
| 內(nèi)存管理 | 基于 LRU 算法 | 結(jié)合 W-TinyLFU 算法(高命中率) |
| 過期策略 | 支持 expireAfterWrite/access | 支持 expireAfterWrite/access + refresh |
| 緩存回收 | 同步阻塞 | 異步非阻塞(后臺線程) |
| 監(jiān)控統(tǒng)計 | 基礎(chǔ)統(tǒng)計(命中率等) | 詳細統(tǒng)計(命中率、加載時間等) |
| 依賴 | 需引入整個 Guava 庫 | 輕量(僅依賴 Caffeine) |
| 社區(qū)維護 | 維護模式(新功能少) | 活躍更新(Java 17+ 兼容) |
從上面的比較可知, Caffeine 各方面是優(yōu)于Guava的,因此在搭建多級緩存機制時,建議使用Caffeine+Redis的組合方案。
業(yè)務執(zhí)行流程:
- 請求優(yōu)先讀取 Caffeine 本地緩存(超快,減少網(wǎng)絡IO)。
- 本地緩存未命中 → 讀取 Redis 分布式緩存。
- Redis 未命中 → 查詢數(shù)據(jù)庫,并回填到兩級緩存。
下面介紹下實現(xiàn)方式
注意:下面的實現(xiàn)方式是基于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
# 端口,默認為6379
port: 6379
# 數(shù)據(jù)庫索引
database: 0
# 密碼
password: abc123
# 連接超時時間
timeout: 6000ms # 連接超時時長(毫秒)
jedis:
pool:
max-active: 1000 # 連接池最大連接數(shù)(使用負值表示沒有限制)
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 {
@Bean
public 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;
}
@Bean
public 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;
}
@Bean
public RedisCacheManager redisCacheManager(
RedisConnectionFactory redisConnectionFactory,
RedisCacheConfiguration cacheConfiguration) {
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}
@Bean
public CacheManager compositeCacheManager(
@Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager,
@Qualifier("redisCacheManager") CacheManager redisCacheManager) {
return new CompositeCacheManager(
caffeineCacheManager,
redisCacheManager
);
}
}
4.業(yè)務邏輯層調(diào)用
使用示例:
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
// 優(yōu)先讀本地緩存,其次Redis,最后數(shù)據(jù)庫
@Cacheable(cacheNames = "product", key = "#id")
public Product getProductById(Long id) {
return repository.findById(id).orElseThrow();
}
// 更新數(shù)據(jù)時清除兩級緩存
@CacheEvict(cacheNames = "product", key = "#product.id")
public Product updateProduct(Product product) {
return repository.save(product);
}
}手動控制多級緩存
@Service
public class CacheService {
@Autowired
private CacheManager cacheManager;
@Autowired
private 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. 查數(shù)據(jù)庫
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 消息通知其他節(jié)點清理本地緩存。
防止緩存擊穿
- Caffeine 配置
refreshAfterWrite:
Caffeine.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(key -> loadFromRedisOrDb(key));監(jiān)控統(tǒng)計:
- Caffeine 統(tǒng)計:cache.getNativeCache().stats()
- Redis 統(tǒng)計:INFO commandstats
驗證多級緩存
- 本地緩存生效:連續(xù)調(diào)用同一接口,觀察第二次響應時間驟降。
- Redis 緩存生效:重啟應用后,首次請求仍快速返回(數(shù)據(jù)來自Redis)。
到此這篇關(guān)于搭建Caffeine+Redis多級緩存機制的文章就介紹到這了,更多相關(guān)Caffeine Redis緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis遍歷海量數(shù)據(jù)集的幾種實現(xiàn)方法
Redis作為一個高性能的鍵值存儲數(shù)據(jù)庫,廣泛應用于各種場景,包括緩存、消息隊列、排行榜,本文主要介紹了Redis遍歷海量數(shù)據(jù)集的幾種實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧2024-02-02
Redis實現(xiàn)每日簽到功能(大數(shù)據(jù)量)
在面對百萬級用戶簽到情況下,傳統(tǒng)數(shù)據(jù)庫存儲和判斷會遇到瓶頸,使用Redis的二進制數(shù)據(jù)類型可實現(xiàn)高效的簽到功能,示例代碼展示了如何調(diào)用這些功能,包括當天簽到、補簽以及查詢簽到記錄,PHP結(jié)合Redis二進制數(shù)據(jù)類型可有效處理大數(shù)據(jù)量下的簽到問題2024-10-10
Redis+Lua腳本實現(xiàn)計數(shù)器接口防刷功能(升級版)
這篇文章主要介紹了Redis+Lua腳本實現(xiàn)計數(shù)器接口防刷功能,使用腳本使得set命令和expire命令一同達到Redis被執(zhí)行且不會被干擾,在很大程度上保證了原子操作,對Redis實現(xiàn)計數(shù)器接口防刷功能感興趣的朋友一起看看吧2022-02-02
?Redis?實現(xiàn)計數(shù)器和限速器的示例代碼
本文主要介紹了?Redis?實現(xiàn)計數(shù)器和限速器的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-02-02

