SpringBoot Caffeine+Redisson配置二級(jí)緩存實(shí)踐
問(wèn)題說(shuō)明
在高性能的服務(wù)架構(gòu)設(shè)計(jì)中,緩存是一個(gè)不可或缺的環(huán)節(jié)。在實(shí)際的項(xiàng)目中,我們通常會(huì)將一些熱點(diǎn)數(shù)據(jù)存儲(chǔ)到Redis或MemCache這類緩存中間件中,只有當(dāng)緩存的訪問(wèn)沒(méi)有命中時(shí)再查詢數(shù)據(jù)庫(kù)。在提升訪問(wèn)速度的同時(shí),也能降低數(shù)據(jù)庫(kù)的壓力。
隨著不斷的發(fā)展,這一架構(gòu)也產(chǎn)生了改進(jìn),在一些場(chǎng)景下可能單純使用Redis類的遠(yuǎn)程緩存已經(jīng)不夠了,還需要進(jìn)一步配合本地緩存使用,例如Guava cache或Caffeine,從而再次提升程序的響應(yīng)速度與服務(wù)性能。于是,就產(chǎn)生了使用本地緩存作為一級(jí)緩存,再加上遠(yuǎn)程緩存作為二級(jí)緩存的兩級(jí)緩存架構(gòu)。

準(zhǔn)備
集成Redission:SpringBoot 整合Redisson重寫(xiě)cacheName支持多參數(shù)
<!--caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>配置文件
增加本地緩存是為了解決高并發(fā)下頻繁查詢Redis的問(wèn)題,配置了expireAfterWrite最后一次寫(xiě)入或訪問(wèn)后經(jīng)過(guò)固定時(shí)間過(guò)期為30秒,例如一次訪問(wèn)一個(gè)頁(yè)面一直刷新,在30秒內(nèi)無(wú)論怎么刷新都會(huì)走本地緩存,而且就算在29秒重新獲取了緩存,也會(huì)重新計(jì)算30秒
@EnableCaching開(kāi)啟緩存功能,也可以加在啟動(dòng)類上
package com.example.redisson.config;
import com.example.redisson.manager.PlusSpringCacheManager;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 緩存配置
*
* @author Lion Li
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* caffeine 本地緩存處理器
*/
@Bean
public Cache<Object, Object> caffeine() {
return Caffeine.newBuilder()
// 設(shè)置最后一次寫(xiě)入或訪問(wèn)后經(jīng)過(guò)固定時(shí)間過(guò)期
.expireAfterWrite(30, TimeUnit.SECONDS)
// 初始的緩存空間大小
.initialCapacity(100)
// 緩存的最大條數(shù)
.maximumSize(1000)
.build();
}
/**
* 自定義緩存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new PlusSpringCacheManager();
}
}
裝飾器
- put方法是cache使用的也就是Redission ,會(huì)自動(dòng)加上cacheNames
- 而caffeine不使用cacheNames ,get方法的key是哪個(gè)就會(huì)緩存哪個(gè)key
- 所以為了解決key相同,cacheNames 不同的問(wèn)題增加了getUniqueKey方法
getName( ) = cacheNames
一個(gè)CacheNams對(duì)應(yīng)一個(gè)Cache對(duì)象
package org.dromara.common.redis.manager;
import cn.hutool.core.lang.Console;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.cache.Cache;
import java.util.concurrent.Callable;
/**
* Cache 裝飾器模式(用于擴(kuò)展 Caffeine 一級(jí)緩存)
*
* @author LionLi
*/
public class CaffeineCacheDecorator implements Cache {
private static final com.github.benmanes.caffeine.cache.Cache<Object, Object>
CAFFEINE = SpringUtils.getBean("caffeine");
private final Cache cache;
public CaffeineCacheDecorator(Cache cache) {
this.cache = cache;
}
@Override
public String getName() {
return cache.getName();
}
@Override
public Object getNativeCache() {
return cache.getNativeCache();
}
public String getUniqueKey(Object key) {
return cache.getName() + ":" + key;
}
@Override
public ValueWrapper get(Object key) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key));
Console.log("redisson caffeine -> key: " + getUniqueKey(key) + ",value:" + o);
return (ValueWrapper) o;
}
@SuppressWarnings("unchecked")
public <T> T get(Object key, Class<T> type) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
Console.log("redisson caffeine -> key: " + getUniqueKey(key) + ",value:" + o);
return (T) o;
}
@Override
public void put(Object key, Object value) {
cache.put(key, value);
}
public ValueWrapper putIfAbsent(Object key, Object value) {
return cache.putIfAbsent(key, value);
}
@Override
public void evict(Object key) {
evictIfPresent(key);
}
public boolean evictIfPresent(Object key) {
boolean b = cache.evictIfPresent(key);
if (b) {
CAFFEINE.invalidate(getUniqueKey(key));
}
return b;
}
@Override
public void clear() {
cache.clear();
}
public boolean invalidate() {
return cache.invalidate();
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
Console.log("redisson caffeine -> key: " + getUniqueKey(key) + ",value:" + o);
return (T) o;
}
}
修改自定義PlusSpringCacheManager,注入裝飾器

使用
@Cacheable(cacheNames = "demo:cache#60s#10m#20", key = "#key", condition = "#key != null")
@GetMapping("/test1")
public R<String> test1(String key, String value) {
System.out.println("test1-->調(diào)用方法體");
return R.ok("操作成功", value);
}
因?yàn)?code>@Cacheable注解上來(lái)就會(huì)調(diào)用get方法,所以可以觸發(fā)本地緩存,@CachePut注解則不行

查看源碼可知,如果一級(jí)沒(méi)有獲取到,則獲取二級(jí)

緩存工具類
package com.example.redisson.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.redisson.api.RMap;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import java.util.Set;
/**
* 緩存操作工具類 {@link }
*
* @author Michelle.Chung
* @date 2022/8/13
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings(value = {"unchecked"})
public class CacheUtils {
private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);
/**
* 獲取緩存組內(nèi)所有的KEY
*
* @param cacheNames 緩存組名稱
*/
public static Set<Object> keys(String cacheNames) {
RMap<Object, Object> rmap = (RMap<Object, Object>) CACHE_MANAGER.getCache(cacheNames).getNativeCache();
return rmap.keySet();
}
/**
* 獲取緩存值
*
* @param cacheNames 緩存組名稱
* @param key 緩存key
*/
public static <T> T get(String cacheNames, Object key) {
Cache.ValueWrapper wrapper = CACHE_MANAGER.getCache(cacheNames).get(key);
return wrapper != null ? (T) wrapper.get() : null;
}
/**
* 保存緩存值
*
* @param cacheNames 緩存組名稱
* @param key 緩存key
* @param value 緩存值
*/
public static void put(String cacheNames, Object key, Object value) {
CACHE_MANAGER.getCache(cacheNames).put(key, value);
}
/**
* 刪除緩存值
*
* @param cacheNames 緩存組名稱
* @param key 緩存key
*/
public static void evict(String cacheNames, Object key) {
CACHE_MANAGER.getCache(cacheNames).evict(key);
}
/**
* 清空緩存值
*
* @param cacheNames 緩存組名稱
*/
public static void clear(String cacheNames) {
CACHE_MANAGER.getCache(cacheNames).clear();
}
}
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
一文帶你掌握J(rèn)ava?SPI的原理和實(shí)踐
在Java中,我們經(jīng)常會(huì)提到面向接口編程,這樣減少了模塊之間的耦合,更加靈活,Java?SPI?(Service?Provider?Interface)就提供了這樣的機(jī)制,本文就來(lái)講講它的原理與具體使用吧2023-05-05
spring mvc 實(shí)現(xiàn)獲取后端傳遞的值操作示例
這篇文章主要介紹了spring mvc 實(shí)現(xiàn)獲取后端傳遞的值操作,結(jié)合實(shí)例形式詳細(xì)分析了spring mvc使用JSTL 方法獲取后端傳遞的值相關(guān)操作技巧2019-11-11
Java并發(fā)編程Callable與Future的應(yīng)用實(shí)例代碼
這篇文章主要介紹了Java并發(fā)編程Callable與Future的應(yīng)用實(shí)例代碼,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
SpringBoot動(dòng)態(tài)修改yml配置文件的方法詳解
這篇文章主要為大家詳細(xì)介紹了SpringBoot動(dòng)態(tài)修改yml配置文件的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03
Java實(shí)題演練二叉搜索樹(shù)與雙向鏈表分析
這篇文章主要介紹了Java二叉搜索樹(shù)與雙向鏈表,總的來(lái)說(shuō)這并不是一道難題,那為什么要拿出這道題介紹?拿出這道題真正想要傳達(dá)的是解題的思路,以及不斷優(yōu)化探尋最優(yōu)解的過(guò)程。希望通過(guò)這道題能給你帶來(lái)一種解題優(yōu)化的思路2022-12-12
springcloud中Feign超時(shí)提示Read timed out executing
Feign接口調(diào)用分兩層,Ribbon的調(diào)用和Hystrix調(diào)用,理論上設(shè)置Ribbon的時(shí)間即可,但是Ribbon的超時(shí)時(shí)間和Hystrix的超時(shí)時(shí)間需要結(jié)合起來(lái),這篇文章給大家介紹springcloud之Feign超時(shí)提示Read timed out executing POST問(wèn)題及解決方法,感興趣的朋友一起看看吧2024-01-01

