<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在生產中已有實踐,本元件僅做個人學習交流分享使用。github:https://github.com/axinSoochow/redis-caffeine-cache-starter
個人水平有限,歡迎大家在評論區輕噴。
快取就是將資料從讀取較慢的媒介上讀取出來放到讀取較快的媒介上,如磁碟-->記憶體。
平時我們會將資料儲存到磁碟上,如:資料庫。如果每次都從資料庫裡去讀取,會因為磁碟本身的IO影響讀取速度,所以就有了像redis這種的記憶體快取。可以將資料讀取出來放到記憶體裡,這樣當需要獲取資料時,就能夠直接從記憶體中拿到資料返回,能夠很大程度的提高速度。
但是一般redis是單獨部署成叢集,所以會有網路IO上的消耗,雖然與redis叢集的連結已經有連線池這種工具,但是資料傳輸上也還是會有一定消耗。所以就有了程序內快取,如:caffeine。當應用內快取有符合條件的資料時,就可以直接使用,而不用通過網路到redis中去獲取,這樣就形成了兩級快取。應用內快取叫做一級快取,遠端快取(如redis)叫做二級快取。
Redis用來儲存熱點資料,Redis中沒有的資料則直接去資料庫存取。
已經有Redis了,幹嘛還需要了解Guava,Caffeine這些程序快取呢:
所以如果僅僅是使用Redis,能滿足我們大部分需求,但是當需要追求更高的效能以及更高的可用性的時候,那就不得不瞭解多級快取。
二級快取操作過程資料讀流程描述
redis 與本地快取都查詢不到值的時候,會觸發更新過程,整個過程是加鎖的快取失效流程描述
redis更新與刪除快取key都會觸發,清除redis快取後
元件是基於Spring Cache框架上改造的,在專案中使用分散式快取,僅僅需要在快取註解上增加:cacheManager ="L2_CacheManager",或者 cacheManager = CacheRedisCaffeineAutoConfiguration.分散式二級快取
//這個方法會使用分散式二級快取來提供查詢 @Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager") public Config getAllValidateConfig() { }
如果你想既使用分散式快取,又想用分散式二級快取元件,那你需要向Spring注入一個 @Primary 的 CacheManager bean
@Primary @Bean("deaultCacheManager") public RedisCacheManager cacheManager(RedisConnectionFactory factory) { // 生成一個預設設定,通過config物件即可對快取進行自定義設定 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 設定快取的預設過期時間,也是使用Duration設定 config = config.entryTtl(Duration.ofMinutes(2)).disableCachingNullValues(); // 設定一個初始化的快取空間set集合 Set<String> cacheNames = new HashSet<>(); cacheNames.add(CacheNames.CACHE_15MINS); cacheNames.add(CacheNames.CACHE_30MINS); // 對每個快取空間應用不同的設定 Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put(CacheNames.CACHE_15MINS, config.entryTtl(Duration.ofMinutes(15))); configMap.put(CacheNames.CACHE_30MINS, config.entryTtl(Duration.ofMinutes(30))); // 使用自定義的快取設定初始化一個cacheManager RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .initialCacheNames(cacheNames) // 注意這兩句的呼叫順序,一定要先呼叫該方法設定初始化的快取名,再初始化相關的設定 .withInitialCacheConfigurations(configMap) .build(); return cacheManager; }
然後:
//這個方法會使用分散式二級快取 @Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager") public Config getAllValidateConfig() { } //這個方法會使用分散式快取 @Cacheable(cacheNames = CacheNames.CACHE_12HOUR) public Config getAllValidateConfig2() { }
核心其實就是實現 org.springframework.cache.CacheManager介面與繼承org.springframework.cache.support.AbstractValueAdaptingCache,在Spring快取框架下實現快取的讀與寫。
RedisCaffeineCacheManager實現CacheManager 介面
RedisCaffeineCacheManager.class 主要來管理快取範例,根據不同的 CacheNames 生成對應的快取管理bean,然後放入一個map中。
package com.axin.idea.rediscaffeinecachestarter.support; import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.stats.CacheStats; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.CollectionUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @Slf4j public class RedisCaffeineCacheManager implements CacheManager { private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class); private static ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(); private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; private RedisTemplate<Object, Object> stringKeyRedisTemplate; private boolean dynamic = true; private Set<String> cacheNames; { cacheNames = new HashSet<>(); cacheNames.add(CacheNames.CACHE_15MINS); cacheNames.add(CacheNames.CACHE_30MINS); cacheNames.add(CacheNames.CACHE_60MINS); cacheNames.add(CacheNames.CACHE_180MINS); cacheNames.add(CacheNames.CACHE_12HOUR); } public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties, RedisTemplate<Object, Object> stringKeyRedisTemplate) { super(); this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties; this.stringKeyRedisTemplate = stringKeyRedisTemplate; this.dynamic = cacheRedisCaffeineProperties.isDynamic(); } //——————————————————————— 進行快取工具 —————————————————————— /** * 清除所有程序快取 */ public void clearAllCache() { stringKeyRedisTemplate.convertAndSend(cacheRedisCaffeineProperties.getRedis().getTopic(), new CacheMessage(null, null)); } /** * 返回所有程序快取(二級快取)的統計資訊 * result:{"快取名稱":統計資訊} * @return */ public static Map<String, CacheStats> getCacheStats() { if (CollectionUtils.isEmpty(cacheMap)) { return null; } Map<String, CacheStats> result = new LinkedHashMap<>(); for (Cache cache : cacheMap.values()) { RedisCaffeineCache caffeineCache = (RedisCaffeineCache) cache; result.put(caffeineCache.getName(), caffeineCache.getCaffeineCache().stats()); } return result; } //—————————————————————————— core ————————————————————————— @Override public Cache getCache(String name) { Cache cache = cacheMap.get(name); if(cache != null) { return cache; } if(!dynamic && !cacheNames.contains(name)) { return null; } cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(name), cacheRedisCaffeineProperties); Cache oldCache = cacheMap.putIfAbsent(name, cache); logger.debug("create cache instance, the cache name is : {}", name); return oldCache == null ? cache : oldCache; } @Override public Collection<String> getCacheNames() { return this.cacheNames; } public void clearLocal(String cacheName, Object key) { //cacheName為null 清除所有程序快取 if (cacheName == null) { log.info("清除所有本地快取"); cacheMap = new ConcurrentHashMap<>(); return; } Cache cache = cacheMap.get(cacheName); if(cache == null) { return; } RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache; redisCaffeineCache.clearLocal(key); } /** * 範例化本地一級快取 * @param name * @return */ private com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(String name) { Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder(); CacheRedisCaffeineProperties.CacheDefault cacheConfig; switch (name) { case CacheNames.CACHE_15MINS: cacheConfig = cacheRedisCaffeineProperties.getCache15m(); break; case CacheNames.CACHE_30MINS: cacheConfig = cacheRedisCaffeineProperties.getCache30m(); break; case CacheNames.CACHE_60MINS: cacheConfig = cacheRedisCaffeineProperties.getCache60m(); break; case CacheNames.CACHE_180MINS: cacheConfig = cacheRedisCaffeineProperties.getCache180m(); break; case CacheNames.CACHE_12HOUR: cacheConfig = cacheRedisCaffeineProperties.getCache12h(); break; default: cacheConfig = cacheRedisCaffeineProperties.getCacheDefault(); } long expireAfterAccess = cacheConfig.getExpireAfterAccess(); long expireAfterWrite = cacheConfig.getExpireAfterWrite(); int initialCapacity = cacheConfig.getInitialCapacity(); long maximumSize = cacheConfig.getMaximumSize(); long refreshAfterWrite = cacheConfig.getRefreshAfterWrite(); log.debug("本地快取初始化:"); if (expireAfterAccess > 0) { log.debug("設定本地快取存取後過期時間,{}秒", expireAfterAccess); cacheBuilder.expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS); } if (expireAfterWrite > 0) { log.debug("設定本地快取寫入後過期時間,{}秒", expireAfterWrite); cacheBuilder.expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS); } if (initialCapacity > 0) { log.debug("設定快取初始化大小{}", initialCapacity); cacheBuilder.initialCapacity(initialCapacity); } if (maximumSize > 0) { log.debug("設定本地快取最大值{}", maximumSize); cacheBuilder.maximumSize(maximumSize); } if (refreshAfterWrite > 0) { cacheBuilder.refreshAfterWrite(refreshAfterWrite, TimeUnit.SECONDS); } cacheBuilder.recordStats(); return cacheBuilder.build(); } }
RedisCaffeineCache 繼承 AbstractValueAdaptingCache
核心是get方法與put方法。
package com.axin.idea.rediscaffeinecachestarter.support; import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties; import com.github.benmanes.caffeine.cache.Cache; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.support.AbstractValueAdaptingCache; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.StringUtils; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class RedisCaffeineCache extends AbstractValueAdaptingCache { private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class); private String name; private RedisTemplate<Object, Object> redisTemplate; @Getter private Cache<Object, Object> caffeineCache; private String cachePrefix; /** * 預設key超時時間 3600s */ private long defaultExpiration = 3600; private Map<String, Long> defaultExpires = new HashMap<>(); { defaultExpires.put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15)); defaultExpires.put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30)); defaultExpires.put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60)); defaultExpires.put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180)); defaultExpires.put(CacheNames.CACHE_12HOUR, TimeUnit.HOURS.toSeconds(12)); } private String topic; private Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap(); protected RedisCaffeineCache(boolean allowNullValues) { super(allowNullValues); } public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) { super(cacheRedisCaffeineProperties.isCacheNullValues()); this.name = name; this.redisTemplate = redisTemplate; this.caffeineCache = caffeineCache; this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix(); this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration(); this.topic = cacheRedisCaffeineProperties.getRedis().getTopic(); defaultExpires.putAll(cacheRedisCaffeineProperties.getRedis().getExpires()); } @Override public String getName() { return this.name; } @Override public Object getNativeCache() { return this; } @Override public <T> T get(Object key, Callable<T> valueLoader) { Object value = lookup(key); if (value != null) { return (T) value; } //key在redis和快取中均不存在 ReentrantLock lock = keyLockMap.get(key.toString()); if (lock == null) { logger.debug("create lock for key : {}", key); keyLockMap.putIfAbsent(key.toString(), new ReentrantLock()); lock = keyLockMap.get(key.toString()); } try { lock.lock(); value = lookup(key); if (value != null) { return (T) value; } //執行原方法獲得value value = valueLoader.call(); Object storeValue = toStoreValue(value); put(key, storeValue); return (T) value; } catch (Exception e) { throw new ValueRetrievalException(key, valueLoader, e.getCause()); } finally { lock.unlock(); } } @Override public void put(Object key, Object value) { if (!super.isAllowNullValues() && value == null) { this.evict(key); return; } long expire = getExpire(); logger.debug("put:{},expire:{}", getKey(key), expire); redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.SECONDS); //快取變更時通知其他節點清理本地快取 push(new CacheMessage(this.name, key)); //此處put沒有意義,會收到自己傳送的快取key失效訊息 // caffeineCache.put(key, value); } @Override public ValueWrapper putIfAbsent(Object key, Object value) { Object cacheKey = getKey(key); // 使用setIfAbsent原子性操作 long expire = getExpire(); boolean setSuccess; setSuccess = redisTemplate.opsForValue().setIfAbsent(getKey(key), toStoreValue(value), Duration.ofSeconds(expire)); Object hasValue; //setNx結果 if (setSuccess) { push(new CacheMessage(this.name, key)); hasValue = value; }else { hasValue = redisTemplate.opsForValue().get(cacheKey); } caffeineCache.put(key, toStoreValue(value)); return toValueWrapper(hasValue); } @Override public void evict(Object key) { // 先清除redis中快取資料,然後清除caffeine中的快取,避免短時間內如果先清除caffeine快取後其他請求會再從redis裡載入到caffeine中 redisTemplate.delete(getKey(key)); push(new CacheMessage(this.name, key)); caffeineCache.invalidate(key); } @Override public void clear() { // 先清除redis中快取資料,然後清除caffeine中的快取,避免短時間內如果先清除caffeine快取後其他請求會再從redis裡載入到caffeine中 Set<Object> keys = redisTemplate.keys(this.name.concat(":*")); for (Object key : keys) { redisTemplate.delete(key); } push(new CacheMessage(this.name, null)); caffeineCache.invalidateAll(); } /** * 取值邏輯 * @param key * @return */ @Override protected Object lookup(Object key) { Object cacheKey = getKey(key); Object value = caffeineCache.getIfPresent(key); if (value != null) { logger.debug("從本地快取中獲得key, the key is : {}", cacheKey); return value; } value = redisTemplate.opsForValue().get(cacheKey); if (value != null) { logger.debug("從redis中獲得值,將值放到本地快取中, the key is : {}", cacheKey); caffeineCache.put(key, value); } return value; } /** * @description 清理本地快取 */ public void clearLocal(Object key) { logger.debug("clear local cache, the key is : {}", key); if (key == null) { caffeineCache.invalidateAll(); } else { caffeineCache.invalidate(key); } } //————————————————————————————私有方法—————————————————————————— private Object getKey(Object key) { String keyStr = this.name.concat(":").concat(key.toString()); return StringUtils.isEmpty(this.cachePrefix) ? keyStr : this.cachePrefix.concat(":").concat(keyStr); } private long getExpire() { long expire = defaultExpiration; Long cacheNameExpire = defaultExpires.get(this.name); return cacheNameExpire == null ? expire : cacheNameExpire.longValue(); } /** * @description 快取變更時通知其他節點清理本地快取 */ private void push(CacheMessage message) { redisTemplate.convertAndSend(topic, message); } }
現在的線上生產的都是多個節點,如果本節點的快取失效了,是需要通過中介軟體來通知其他節點失效訊息的。本元件考慮到學習分享讓大家引入的依賴少點,就直接通過 redis 來傳送訊息了,實際生產過程中換成成熟的訊息中介軟體(kafka、RocketMQ)來做通知更為穩妥。
到此這篇關於Redis+Caffeine實現分散式二級快取元件實戰教學的文章就介紹到這了,更多相關Redis Caffeine分散式二級快取內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45