SpringBoot多級緩存實現方案總結
1.背景
緩存,就是讓數據更接近使用者,讓訪問速度加快,從而提升系統(tǒng)性能。工作機制大概是先從緩存中加載數據,如果沒有,再從慢速設備(eg:數據庫)中加載數據并同步到緩存中。
所謂多級緩存,是指在整個系統(tǒng)架構的不同系統(tǒng)層面進行數據緩存,以提升訪問速度。主要分為三層緩存:網關nginx緩存、分布式緩存、本地緩存。這里的多級緩存就是用redis分布式緩存+caffeine本地緩存整合而來。
平時我們在開發(fā)過程中,一般都是使用redis實現分布式緩存、caffeine操作本地緩存,但是發(fā)現只使用redis或者是caffeine實現緩存都有一些問題:
- 一級緩存:Caffeine是一個一個高性能的 Java 緩存庫;使用 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。優(yōu)點數據就在應用內存所以速度快。缺點受應用內存的限制,所以容量有限;沒有持久化,重啟服務后緩存數據會丟失;在分布式環(huán)境下緩存數據數據無法同步;
- 二級緩存:redis是一高性能、高可用的key-value數據庫,支持多種數據類型,支持集群,和應用服務器分開部署易于橫向擴展。優(yōu)點支持多種數據類型,擴容方便;有持久化,重啟應用服務器緩存數據不會丟失;他是一個集中式緩存,不存在在應用服務器之間同步數據的問題。缺點每次都需要訪問redis存在IO浪費的情況。
綜上所述,我們可以通過整合redis和caffeine實現多級緩存,解決上面單一緩存的痛點,從而做到相互補足。
2.整合實現
2.1思路
Spring 本來就提供了Cache的支持,最核心的就是實現Cache和CacheManager接口。但是Spring Cache存在以下問題:
- Spring Cache 僅支持單一的緩存來源,即:只能選擇 Redis 實現或者 Caffeine 實現,并不能同時使用。
- 數據一致性:各層緩存之間的數據一致性問題,如應用層緩存和分布式緩存之前的數據一致性問題。
由此我們可以通過重新實現Cache和CacheManager接口,整合redis和caffeine,從而實現多級緩存。在講實現原理之前先看看多級緩存調用邏輯圖:
2.2實現
首先,我們需要一個多級緩存配置類,方便對緩存屬性的動態(tài)配置,通過開關做到可插拔。
@ConfigurationProperties(prefix = "multilevel.cache") @Data public class MultilevelCacheProperties { ? ? ?/** ? ? * 一級本地緩存最大比例 ? ? */ ? ?private Double maxCapacityRate = 0.2; ? ? ?/** ? ? * 一級本地緩存與最大緩存初始化大小比例 ? ? */ ? ?private Double initRate = 0.5; ? ? ?/** ? ? * 消息主題 ? ? */ ? ?private String topic = "multilevel-cache-topic"; ? ? ?/** ? ? * 緩存名稱 ? ? */ ? ?private String name = "multilevel-cache"; ? ? ?/** ? ? * 一級本地緩存名稱 ? ? */ ? ?private String caffeineName = "multilevel-caffeine-cache"; ? ? ?/** ? ? * 二級緩存名稱 ? ? */ ? ?private String redisName = "multilevel-redis-cache"; ? ? ?/** ? ? * 一級本地緩存過期時間 ? ? */ ? ?private Integer caffeineExpireTime = 300; ? ? ?/** ? ? * 二級緩存過期時間 ? ? */ ? ?private Integer redisExpireTime = 600; ? ? ? ?/** ? ? * 一級緩存開關 ? ? */ ? ?private Boolean caffeineSwitch = true; ? }
在自動配置類使用 @EnableConfigurationProperties(MultilevelCacheProperties.class)
注入即可使用。
接下來就是重新實現spring的Cache接口,整合caffeine本地緩存和redis分布式緩存實現多級緩存
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緩存的對象object必須序列化 implements Serializable, 不然緩存對象不成功。 ? ? * 注意:這里asyncPublish()方法是異步發(fā)布消息,然后讓分布式其他節(jié)點清除本地緩存,防止當前節(jié)點因更新覆蓋數據而其他節(jié)點本地緩存保存是臟數據 ? ? * 這樣本地緩存數據才能成功存入 ? ? * @param key ? ? * @param value ? ? */ ? ?@Override ? ?public void put(@NonNull Object key, Object value) { ? ? ? ?redisCache.put(key, value); ? ? ? ?// 異步清除本地緩存 ? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) { ? ? ? ? ? ?asyncPublish(key, value); ? ? ? } ? } ? ? ?/** ? ? * key不存在時,再保存,存在返回當前值不覆蓋 ? ? * @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中緩存數據,然后通過消息推送清除所有節(jié)點caffeine中的緩存, ? ? ? ?// 避免短時間內如果先清除caffeine緩存后其他請求會再從redis里加載到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()) { ? ? ? ? ? ?// 開啟一級緩存,先從一級緩存緩存數據 ? ? ? ? ? ?value = caffeineCache.get(key); ? ? ? ? ? ?if (Objects.nonNull(value)) { ? ? ? ? ? ? ? ?log.info("查詢caffeine 一級緩存 key:{}, 返回值是:{}", key, value.get()); ? ? ? ? ? ? ? ?return value.get(); ? ? ? ? ? } ? ? ? } ? ? ? ?value = redisCache.get(key); ? ? ? ?if (Objects.nonNull(value)) { ? ? ? ? ? ?log.info("查詢redis 二級緩存 key:{}, 返回值是:{}", key, value.get()); ? ? ? ? ? ?// 異步將二級緩存redis寫到一級緩存caffeine ? ? ? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) { ? ? ? ? ? ? ? ?ValueWrapper finalValue = value; ? ? ? ? ? ? ? ?cacheExecutor.execute(()->{ ? ? ? ? ? ? ? ? ? ?caffeineCache.put(key, finalValue.get()); ? ? ? ? ? ? ? }); ? ? ? ? ? } ? ? ? ? ? ?return value.get(); ? ? ? } ? ? ? ?return null; ? } ? ? ?/** ? ? * 緩存變更時通知其他節(jié)點清理本地緩存 ? ? * 異步通過發(fā)布訂閱主題消息,其他節(jié)點監(jiān)聽到之后進行相關本地緩存操作,防止本地緩存臟數據 ? ? */ ? ?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)聽:我們通監(jiān)聽caffeine鍵值的移除、打印日志方便排查問題,通過監(jiān)聽redis發(fā)布的消息,實現分布式集群多節(jié)點本地緩存清除從而達到數據一致性。
消息體
@Data public class CacheMessage implements Serializable { ? ?private String cacheName; ? ?private Object key; ? ?private Object value; ? ?private Integer type; }
caffeine移除監(jiān)聽:
@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) { ? ? ? ? } ? ? ? ?// 超出過期時間 ? ? ? ?if (cause == RemovalCause.EXPIRED) { ? ? ? ? ? ?// do something ? ? ? } ? ? ? ?// 顯式移除 ? ? ? ?if (cause == RemovalCause.EXPLICIT) { ? ? ? ? ? ?// do something ? ? ? } ? ? ? ?// 舊數據被更新 ? ? ? ?if (cause == RemovalCause.REPLACED) { ? ? ? ? ? ?// do something ? ? ? } ? } } ?
redis消息監(jiān)聽:
@Slf4j @Data public class RedisCacheMessageListener implements MessageListener { ? ? ?private CaffeineCache caffeineCache; ? ?@Override ? ?public void onMessage(Message message, byte[] pattern) { ? ? ? ?log.info("監(jiān)聽的redis message: {}" + message.toString()); ? ? ? ?CacheMessage cacheMessage = JsonUtils.parseObject(message.toString(), CacheMessage.class); ? ? ? ?if (Objects.isNull(cacheMessage.getKey())) { ? ? ? ? ? ?caffeineCache.invalidate(); ? ? ? } else { ? ? ? ? ? ?caffeineCache.evict(cacheMessage.getKey()); ? ? ? } ? } }
最后,通過自動配置類,注入相關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 不會再值過期后立即執(zhí)行清除,而是在寫入或者讀取操作之后執(zhí)行少量維護工作,或者在寫入讀取很少的情況下,偶爾執(zhí)行清除操作。 ? ? * 如果我們項目寫入或者讀取頻率很高,那么不用擔心。如果想入寫入和讀取操作頻率較低,那么我們可以通過Cache.cleanUp()或者加scheduler去定時執(zhí)行清除操作。 ? ? * Scheduler可以迅速刪除過期的元素,***Java 9 +***后的版本,可以通過Scheduler.systemScheduler(), 調用系統(tǒng)線程,達到定期清除的目的 ? ? * @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() ? ? ? ? ? ? ? ?// 設置初始緩存大小 ? ? ? ? ? ? ? .initialCapacity(initCapacity) ? ? ? ? ? ? ? ?// 設置最大緩存 ? ? ? ? ? ? ? .maximumSize(maxCapacity) ? ? ? ? ? ? ? ?// 設置緩存線程池 ? ? ? ? ? ? ? .executor(cacheExecutor) ? ? ? ? ? ? ? ?// 設置定時任務執(zhí)行過期清除操作 // ? ? ? ? ? ? ? .scheduler(Scheduler.systemScheduler()) ? ? ? ? ? ? ? ?// 監(jiān)聽器(超出最大緩存) ? ? ? ? ? ? ? .removalListener(new CaffeineCacheRemovalListener()) ? ? ? ? ? ? ? ?// 設置緩存讀時間的過期時間 ? ? ? ? ? ? ? .expireAfterAccess(Duration.of(multilevelCacheProperties.getCaffeineExpireTime(), ChronoUnit.SECONDS)) ? ? ? ? ? ? ? ?// 開啟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.使用
使用非常簡單,只需要通過 multilevelCache
操作即可:
@RestController @RequestMapping("/api/data") @Api(tags = "api數據") @Slf4j public class ApiDataController { ? ? ?@Resource ? ?private MultilevelCache multilevelCache; ? ? ?@GetMapping("/put/cache") ? ?public void put() { ? ? ? ?DataSource ds = new DataSource(); ? ? ? ?ds.setName("多級緩存"); ? ? ? ?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.總結
以上全部就是關于多級緩存的實現方案總結,多級緩存就是為了解決項目服務中單一緩存使用不足的缺點。應用場景有:接口權限校驗,每次請求接口都需要根據當前登錄人有哪些角色,角色有哪些權限,如果每次都去查數據庫性能開銷比較嚴重,再加上權限一般不怎么會頻繁變更,所以使用多級緩存是最合適不過了;還有就是很多管理系統(tǒng)列表界面都有組織架構信息(所屬部門、小組等),這些信息同樣可以使用多級緩存來完美提升性能。
到此這篇關于SpringBoot多級緩存實現方案總結的文章就介紹到這了,更多相關SpringBoot實現多級緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
spring security登錄成功后通過Principal獲取名返回空問題
這篇文章主要介紹了spring security登錄成功后通過Principal獲取名返回空問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03SpringBoot整合WebService的實戰(zhàn)案例
WebService是一個SOA(面向服務的編程)的架構,它是不依賴于語言,平臺等,可以實現不同的語言間的相互調用,這篇文章主要給大家介紹了關于SpringBoot整合WebService的相關資料,需要的朋友可以參考下2024-07-07SpringBoot2.6.x默認禁用循環(huán)依賴后的問題解決
由于SpringBoot從底層逐漸引導開發(fā)者書寫規(guī)范的代碼,同時也是個憂傷的消息,循環(huán)依賴的應用場景實在是太廣泛了,所以SpringBoot 2.6.x不推薦使用循環(huán)依賴,本文給大家說下SpringBoot2.6.x默認禁用循環(huán)依賴后的應對策略,感興趣的朋友一起看看吧2022-02-02