1.背景
緩存,就是讓數(shù)據(jù)更接近使用者,讓訪(fǎng)問(wèn)速度加快,從而提升系統(tǒng)性能。工作機(jī)制大概是先從緩存中加載數(shù)據(jù),如果沒(méi)有,再?gòu)穆僭O(shè)備(eg:數(shù)據(jù)庫(kù))中加載數(shù)據(jù)并同步到緩存中。
所謂多級(jí)緩存,是指在整個(gè)系統(tǒng)架構(gòu)的不同系統(tǒng)層面進(jìn)行數(shù)據(jù)緩存,以提升訪(fǎng)問(wèn)速度。主要分為三層緩存:網(wǎng)關(guān)nginx緩存、分布式緩存、本地緩存。這里的多級(jí)緩存就是用redis分布式緩存+caffeine本地緩存整合而來(lái)。
平時(shí)我們?cè)陂_(kāi)發(fā)過(guò)程中,一般都是使用redis實(shí)現(xiàn)分布式緩存、caffeine操作本地緩存,但是發(fā)現(xiàn)只使用redis或者是caffeine實(shí)現(xiàn)緩存都有一些問(wèn)題:
- 一級(jí)緩存:Caffeine是一個(gè)一個(gè)高性能的 Java 緩存庫(kù);使用 Window TinyLfu 回收策略,提供了一個(gè)近乎最佳的命中率。優(yōu)點(diǎn)數(shù)據(jù)就在應(yīng)用內(nèi)存所以速度快。缺點(diǎn)受應(yīng)用內(nèi)存的限制,所以容量有限;沒(méi)有持久化,重啟服務(wù)后緩存數(shù)據(jù)會(huì)丟失;在分布式環(huán)境下緩存數(shù)據(jù)數(shù)據(jù)無(wú)法同步;
- 二級(jí)緩存:redis是一高性能、高可用的key-value數(shù)據(jù)庫(kù),支持多種數(shù)據(jù)類(lèi)型,支持集群,和應(yīng)用服務(wù)器分開(kāi)部署易于橫向擴(kuò)展。優(yōu)點(diǎn)支持多種數(shù)據(jù)類(lèi)型,擴(kuò)容方便;有持久化,重啟應(yīng)用服務(wù)器緩存數(shù)據(jù)不會(huì)丟失;他是一個(gè)集中式緩存,不存在在應(yīng)用服務(wù)器之間同步數(shù)據(jù)的問(wèn)題。缺點(diǎn)每次都需要訪(fǎng)問(wèn)redis存在IO浪費(fèi)的情況。
綜上所述,我們可以通過(guò)整合redis和caffeine實(shí)現(xiàn)多級(jí)緩存,解決上面單一緩存的痛點(diǎn),從而做到相互補(bǔ)足。
項(xiàng)目推薦:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企業(yè)級(jí)系統(tǒng)架構(gòu)底層框架封裝,解決業(yè)務(wù)開(kāi)發(fā)時(shí)常見(jiàn)的非功能性需求,防止重復(fù)造輪子,方便業(yè)務(wù)快速開(kāi)發(fā)和企業(yè)技術(shù)??蚣芙y(tǒng)一管理。引入組件化的思想實(shí)現(xiàn)高內(nèi)聚低耦合并且高度可配置化,做到可插拔。嚴(yán)格控制包依賴(lài)和統(tǒng)一版本管理,做到最少化依賴(lài)。注重代碼規(guī)范和注釋?zhuān)浅_m合個(gè)人學(xué)習(xí)和企業(yè)使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公眾號(hào):Shepherd進(jìn)階筆記
交流探討qun:Shepherd_126
2.整合實(shí)現(xiàn)
2.1思路
Spring 本來(lái)就提供了Cache的支持,最核心的就是實(shí)現(xiàn)Cache和CacheManager接口。但是Spring Cache存在以下問(wèn)題:
- Spring Cache 僅支持單一的緩存來(lái)源,即:只能選擇 Redis 實(shí)現(xiàn)或者 Caffeine 實(shí)現(xiàn),并不能同時(shí)使用。
- 數(shù)據(jù)一致性:各層緩存之間的數(shù)據(jù)一致性問(wèn)題,如應(yīng)用層緩存和分布式緩存之前的數(shù)據(jù)一致性問(wèn)題。
由此我們可以通過(guò)重新實(shí)現(xiàn)Cache和CacheManager接口,整合redis和caffeine,從而實(shí)現(xiàn)多級(jí)緩存。在講實(shí)現(xiàn)原理之前先看看多級(jí)緩存調(diào)用邏輯圖:
2.2實(shí)現(xiàn)
首先,我們需要一個(gè)多級(jí)緩存配置類(lèi),方便對(duì)緩存屬性的動(dòng)態(tài)配置,通過(guò)開(kāi)關(guān)做到可插拔。
@ConfigurationProperties(prefix = "multilevel.cache")
@Data
public class MultilevelCacheProperties {
/**
* 一級(jí)本地緩存最大比例
*/
private Double maxCapacityRate = 0.2;
/**
* 一級(jí)本地緩存與最大緩存初始化大小比例
*/
private Double initRate = 0.5;
/**
* 消息主題
*/
private String topic = "multilevel-cache-topic";
/**
* 緩存名稱(chēng)
*/
private String name = "multilevel-cache";
/**
* 一級(jí)本地緩存名稱(chēng)
*/
private String caffeineName = "multilevel-caffeine-cache";
/**
* 二級(jí)緩存名稱(chēng)
*/
private String redisName = "multilevel-redis-cache";
/**
* 一級(jí)本地緩存過(guò)期時(shí)間
*/
private Integer caffeineExpireTime = 300;
/**
* 二級(jí)緩存過(guò)期時(shí)間
*/
private Integer redisExpireTime = 600;
/**
* 一級(jí)緩存開(kāi)關(guān)
*/
private Boolean caffeineSwitch = true;
}
在自動(dòng)配置類(lèi)使用@EnableConfigurationProperties(MultilevelCacheProperties.class)
注入即可使用。
接下來(lái)就是重新實(shí)現(xiàn)spring的Cache接口,整合caffeine本地緩存和redis分布式緩存實(shí)現(xiàn)多級(jí)緩存
package com.plasticene.boot.cache.core.manager;
import com.plasticene.boot.cache.core.listener.CacheMessage;
import com.plasticene.boot.cache.core.prop.MultilevelCacheProperties;
import com.plasticene.boot.common.executor.plasticeneThreadExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.*;
/**
* @author fjzheng
* @version 1.0
* @date 2022/7/20 17:03
*/
@Slf4j
public class MultilevelCache extends AbstractValueAdaptingCache {
@Resource
private MultilevelCacheProperties multilevelCacheProperties;
@Resource
private RedisTemplate redisTemplate;
ExecutorService cacheExecutor = new plasticeneThreadExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 20,
Runtime.getRuntime().availableProcessors() * 200,
"cache-pool"
);
private RedisCache redisCache;
private CaffeineCache caffeineCache;
public MultilevelCache(boolean allowNullValues,RedisCache redisCache, CaffeineCache caffeineCache) {
super(allowNullValues);
this.redisCache = redisCache;
this.caffeineCache = caffeineCache;
}
@Override
public String getName() {
return multilevelCacheProperties.getName();
}
@Override
public Object getNativeCache() {
return null;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
return (T) value;
}
/**
* 注意:redis緩存的對(duì)象object必須序列化 implements Serializable, 不然緩存對(duì)象不成功。
* 注意:這里asyncPublish()方法是異步發(fā)布消息,然后讓分布式其他節(jié)點(diǎn)清除本地緩存,防止當(dāng)前節(jié)點(diǎn)因更新覆蓋數(shù)據(jù)而其他節(jié)點(diǎn)本地緩存保存是臟數(shù)據(jù)
* 這樣本地緩存數(shù)據(jù)才能成功存入
* @param key
* @param value
*/
@Override
public void put(@NonNull Object key, Object value) {
redisCache.put(key, value);
// 異步清除本地緩存
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(key, value);
}
}
/**
* key不存在時(shí),再保存,存在返回當(dāng)前值不覆蓋
* @param key
* @param value
* @return
*/
@Override
public ValueWrapper putIfAbsent(@NonNull Object key, Object value) {
ValueWrapper valueWrapper = redisCache.putIfAbsent(key, value);
// 異步清除本地緩存
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(key, value);
}
return valueWrapper;
}
@Override
public void evict(Object key) {
// 先清除redis中緩存數(shù)據(jù),然后通過(guò)消息推送清除所有節(jié)點(diǎn)caffeine中的緩存,
// 避免短時(shí)間內(nèi)如果先清除caffeine緩存后其他請(qǐng)求會(huì)再?gòu)膔edis里加載到caffeine中
redisCache.evict(key);
// 異步清除本地緩存
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(key, null);
}
}
@Override
public boolean evictIfPresent(Object key) {
return false;
}
@Override
public void clear() {
redisCache.clear();
// 異步清除本地緩存
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(null, null);
}
}
@Override
protected Object lookup(Object key) {
Assert.notNull(key, "key不可為空");
ValueWrapper value;
if (multilevelCacheProperties.getCaffeineSwitch()) {
// 開(kāi)啟一級(jí)緩存,先從一級(jí)緩存緩存數(shù)據(jù)
value = caffeineCache.get(key);
if (Objects.nonNull(value)) {
log.info("查詢(xún)caffeine 一級(jí)緩存 key:{}, 返回值是:{}", key, value.get());
return value.get();
}
}
value = redisCache.get(key);
if (Objects.nonNull(value)) {
log.info("查詢(xún)r(jià)edis 二級(jí)緩存 key:{}, 返回值是:{}", key, value.get());
// 異步將二級(jí)緩存redis寫(xiě)到一級(jí)緩存caffeine
if (multilevelCacheProperties.getCaffeineSwitch()) {
ValueWrapper finalValue = value;
cacheExecutor.execute(()->{
caffeineCache.put(key, finalValue.get());
});
}
return value.get();
}
return null;
}
/**
* 緩存變更時(shí)通知其他節(jié)點(diǎn)清理本地緩存
* 異步通過(guò)發(fā)布訂閱主題消息,其他節(jié)點(diǎn)監(jiān)聽(tīng)到之后進(jìn)行相關(guān)本地緩存操作,防止本地緩存臟數(shù)據(jù)
*/
void asyncPublish(Object key, Object value) {
cacheExecutor.execute(()->{
CacheMessage cacheMessage = new CacheMessage();
cacheMessage.setCacheName(multilevelCacheProperties.getName());
cacheMessage.setKey(key);
cacheMessage.setValue(value);
redisTemplate.convertAndSend(multilevelCacheProperties.getTopic(), cacheMessage);
});
}
}
緩存消息監(jiān)聽(tīng):我們通監(jiān)聽(tīng)caffeine鍵值的移除、打印日志方便排查問(wèn)題,通過(guò)監(jiān)聽(tīng)redis發(fā)布的消息,實(shí)現(xiàn)分布式集群多節(jié)點(diǎn)本地緩存清除從而達(dá)到數(shù)據(jù)一致性。
消息體
@Data
public class CacheMessage implements Serializable {
private String cacheName;
private Object key;
private Object value;
private Integer type;
}
caffeine移除監(jiān)聽(tīng):
@Slf4j
public class CaffeineCacheRemovalListener implements RemovalListener<Object, Object> {
@Override
public void onRemoval(@Nullable Object k, @Nullable Object v, @NonNull RemovalCause cause) {
log.info("[移除緩存] key:{} reason:{}", k, cause.name());
// 超出最大緩存
if (cause == RemovalCause.SIZE) {
}
// 超出過(guò)期時(shí)間
if (cause == RemovalCause.EXPIRED) {
// do something
}
// 顯式移除
if (cause == RemovalCause.EXPLICIT) {
// do something
}
// 舊數(shù)據(jù)被更新
if (cause == RemovalCause.REPLACED) {
// do something
}
}
}
redis消息監(jiān)聽(tīng):
@Slf4j
@Data
public class RedisCacheMessageListener implements MessageListener {
private CaffeineCache caffeineCache;
@Override
public void onMessage(Message message, byte[] pattern) {
log.info("監(jiān)聽(tīng)的redis message: {}" + message.toString());
CacheMessage cacheMessage = JsonUtils.parseObject(message.toString(), CacheMessage.class);
if (Objects.isNull(cacheMessage.getKey())) {
caffeineCache.invalidate();
} else {
caffeineCache.evict(cacheMessage.getKey());
}
}
}
最后,通過(guò)自動(dòng)配置類(lèi),注入相關(guān)bean:
**
* @author fjzheng
* @version 1.0
* @date 2022/7/20 17:24
*/
@Configuration
@EnableConfigurationProperties(MultilevelCacheProperties.class)
public class MultilevelCacheAutoConfiguration {
@Resource
private MultilevelCacheProperties multilevelCacheProperties;
ExecutorService cacheExecutor = new plasticeneThreadExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 20,
Runtime.getRuntime().availableProcessors() * 200,
"cache-pool"
);
@Bean
@ConditionalOnMissingBean({RedisTemplate.class})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return template;
}
@Bean
public RedisCache redisCache (RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.of(multilevelCacheProperties.getRedisExpireTime(), ChronoUnit.SECONDS));
redisCacheConfiguration = redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
RedisCache redisCache = new CustomRedisCache(multilevelCacheProperties.getRedisName(), redisCacheWriter, redisCacheConfiguration);
return redisCache;
}
/**
* 由于Caffeine 不會(huì)再值過(guò)期后立即執(zhí)行清除,而是在寫(xiě)入或者讀取操作之后執(zhí)行少量維護(hù)工作,或者在寫(xiě)入讀取很少的情況下,偶爾執(zhí)行清除操作。
* 如果我們項(xiàng)目寫(xiě)入或者讀取頻率很高,那么不用擔(dān)心。如果想入寫(xiě)入和讀取操作頻率較低,那么我們可以通過(guò)Cache.cleanUp()或者加scheduler去定時(shí)執(zhí)行清除操作。
* Scheduler可以迅速刪除過(guò)期的元素,***Java 9 +***后的版本,可以通過(guò)Scheduler.systemScheduler(), 調(diào)用系統(tǒng)線(xiàn)程,達(dá)到定期清除的目的
* @return
*/
@Bean
@ConditionalOnClass(CaffeineCache.class)
@ConditionalOnProperty(name = "multilevel.cache.caffeineSwitch", havingValue = "true", matchIfMissing = true)
public CaffeineCache caffeineCache() {
int maxCapacity = (int) (Runtime.getRuntime().totalMemory() * multilevelCacheProperties.getMaxCapacityRate());
int initCapacity = (int) (maxCapacity * multilevelCacheProperties.getInitRate());
CaffeineCache caffeineCache = new CaffeineCache(multilevelCacheProperties.getCaffeineName(), Caffeine.newBuilder()
// 設(shè)置初始緩存大小
.initialCapacity(initCapacity)
// 設(shè)置最大緩存
.maximumSize(maxCapacity)
// 設(shè)置緩存線(xiàn)程池
.executor(cacheExecutor)
// 設(shè)置定時(shí)任務(wù)執(zhí)行過(guò)期清除操作
// .scheduler(Scheduler.systemScheduler())
// 監(jiān)聽(tīng)器(超出最大緩存)
.removalListener(new CaffeineCacheRemovalListener())
// 設(shè)置緩存讀時(shí)間的過(guò)期時(shí)間
.expireAfterAccess(Duration.of(multilevelCacheProperties.getCaffeineExpireTime(), ChronoUnit.SECONDS))
// 開(kāi)啟metrics監(jiān)控
.recordStats()
.build());
return caffeineCache;
}
@Bean
@ConditionalOnBean({CaffeineCache.class, RedisCache.class})
public MultilevelCache multilevelCache(RedisCache redisCache, CaffeineCache caffeineCache) {
MultilevelCache multilevelCache = new MultilevelCache(true, redisCache, caffeineCache);
return multilevelCache;
}
@Bean
public RedisCacheMessageListener redisCacheMessageListener(@Autowired CaffeineCache caffeineCache) {
RedisCacheMessageListener redisCacheMessageListener = new RedisCacheMessageListener();
redisCacheMessageListener.setCaffeineCache(caffeineCache);
return redisCacheMessageListener;
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(@Autowired RedisConnectionFactory redisConnectionFactory,
@Autowired RedisCacheMessageListener redisCacheMessageListener) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(redisCacheMessageListener, new ChannelTopic(multilevelCacheProperties.getTopic()));
return redisMessageListenerContainer;
}
}
3.使用
使用非常簡(jiǎn)單,只需要通過(guò)multilevelCache
操作即可:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-633012.html
@RestController
@RequestMapping("/api/data")
@Api(tags = "api數(shù)據(jù)")
@Slf4j
public class ApiDataController {
@Resource
private MultilevelCache multilevelCache;
@GetMapping("/put/cache")
public void put() {
DataSource ds = new DataSource();
ds.setName("多級(jí)緩存");
ds.setType(1);
ds.setCreateTime(new Date());
ds.setHost("127.0.0.1");
multilevelCache.put("test-key", ds);
}
@GetMapping("/get/cache")
public DataSource get() {
DataSource dataSource = multilevelCache.get("test-key", DataSource.class);
return dataSource;
}
}
4.總結(jié)
以上全部就是關(guān)于多級(jí)緩存的實(shí)現(xiàn)方案總結(jié),多級(jí)緩存就是為了解決項(xiàng)目服務(wù)中單一緩存使用不足的缺點(diǎn)。應(yīng)用場(chǎng)景有:接口權(quán)限校驗(yàn),每次請(qǐng)求接口都需要根據(jù)當(dāng)前登錄人有哪些角色,角色有哪些權(quán)限,如果每次都去查數(shù)據(jù)庫(kù)性能開(kāi)銷(xiāo)比較嚴(yán)重,再加上權(quán)限一般不怎么會(huì)頻繁變更,所以使用多級(jí)緩存是最合適不過(guò)了;還有就是很多管理系統(tǒng)列表界面都有組織架構(gòu)信息(所屬部門(mén)、小組等),這些信息同樣可以使用多級(jí)緩存來(lái)完美提升性能。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-633012.html
到了這里,關(guān)于Spring Boot多級(jí)緩存實(shí)現(xiàn)方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!