SpringBoot 2聚谁,用200行代碼完成一個一二級分布式緩存

緩存系統(tǒng)的用來代替直接訪問數(shù)據(jù)庫,用來提升系統(tǒng)性能滞诺,減小數(shù)據(jù)庫負載形导。早期緩存跟系統(tǒng)在一個虛擬機里环疼,這樣內(nèi)存訪問,速度最快朵耕。 后來應用系統(tǒng)水平擴展炫隶,緩存作為一個獨立系統(tǒng)存在,如redis阎曹,但是每次從緩存獲取數(shù)據(jù)伪阶,都還是要通過網(wǎng)絡訪問才能獲取,效率相對于早先從內(nèi)存里獲取处嫌,還是不夠逆天快栅贴。如果一個應用,比如傳統(tǒng)的企業(yè)應用熏迹,一次頁面顯示檐薯,要訪問數(shù)次redis,那效果就不是特別好注暗,性能不夠快不說坛缕,還容易使得Reids負載過高,Redis的主機出現(xiàn)各種物理故障捆昏。因此赚楚,現(xiàn)在有人提出了一二級緩存。即一級緩存跟系統(tǒng)在一個虛擬機內(nèi)骗卜,這樣速度最快宠页。二級緩存位于redis里,當一級緩存沒有數(shù)據(jù)的時候膨俐,再從redis里獲取勇皇,并同步到一級緩存里罩句。這跟CPU的一級緩存焚刺,二級緩存是一個道理。當然也面對同樣的問題门烂。

緩存概念

Cache 通常有如下組件構(gòu)成

  • CacheManager乳愉,用來創(chuàng)建,管理屯远,管理多個命名唯一的Cache蔓姚。如可以有組織機構(gòu)緩存,菜單項的緩存慨丐,菜單樹的緩存等
  • Cache類似Map那樣的Key—Value存儲結(jié)構(gòu)坡脐,Value部分 通常包含了緩存的對象,通過Key來取得緩存對象
  • 緩存項房揭,存放在緩存里的對象备闲,常常需要實現(xiàn)序列化接口晌端,以支持分布式緩存。
  • Cache存儲方式恬砂,緩存組件的可以將對象放到內(nèi)存咧纠,也可以是其他緩存服務器,Spring Boot 提供了一個基于ConcurrentMap的緩存泻骤,同時也集成了Redis,EhCache 2.x,JCache緩存服務器等
  • 緩存策略漆羔,通常Cache 還可以有不同的緩存策略,如設置緩存最大的容量狱掂,緩存項的過期時間等
  • 分布式緩存演痒,緩存通常按照緩存數(shù)據(jù)類型存放在不同緩存服務器上,或者同一類型的緩存趋惨,按照某種算法嫡霞,不同key的數(shù)據(jù)放在不同的緩存服務器上。
  • Cache Hit希柿,當從Cache中取得期望的緩存項诊沪,我們通常稱之為緩存命中。如果沒有命中我們稱之為Cache Miss曾撤,意味著需要從數(shù)據(jù)來源處重新取出并放回Cache中
  • Cache Miss:緩存丟失端姚,根據(jù)Key沒有從緩存中找到對應的緩存項
  • Cache Evication:緩存清除操作。
  • Hot Data挤悉,熱點數(shù)據(jù)渐裸,緩存系統(tǒng)能調(diào)整算法或者內(nèi)部存儲方式,使得將最有可能頻繁訪問的數(shù)據(jù)能盡快訪問到装悲。
  • On-Heap,Java分配對象都是在堆內(nèi)存里昏鹃,有最快的獲取速度。由于虛擬機的垃圾回收管理诀诊,緩存放過多的對象會導致垃圾回收時間過長洞渤,從而有可能影響性能。
  • Off-Heap,堆外內(nèi)存属瓣,對象存放到在虛擬機分配的堆外內(nèi)存载迄,因此不受垃圾回收管理的管理,不影響系統(tǒng)系統(tǒng)抡蛙,但堆外內(nèi)存的對象要被使用护昧,還要序列化成堆內(nèi)對象。很多緩存工具會把不常用的對象放到堆外粗截,把熱點數(shù)據(jù)放到堆內(nèi)惋耙。

Spring Boot 緩存

Spring Boot 本身提供了一個基于ConcurrentHashMap 的緩存機制,也集成了EhCache2.x,JCache(JSR-107,EhCache3.x,Hazelcast,Infinispan),還有Couchbase绽榛,Redies等遥金。Spring Boot應用通過注解的方式使用統(tǒng)一的使用緩存,只需在方法上使用緩存注解即可蒜田,其緩存的具體實現(xiàn)依賴于你選擇的目標緩存管理器稿械。如下使用@Cacheable

    @Service
    public class MenuServiceImpl implements MenuService {
        
        @Cacheable("menu")
        public Menu getMenu(Long id) {...}
            
    }

MenuService實例作為一個容器管理bean,Spring將會生成代理類冲粤,在實際調(diào)用MenuService.getMenu方法前美莫,會調(diào)用緩存管理器,取得名"menu"的緩存梯捕,此時厢呵,緩存的key就是方法參數(shù)id,如果緩存命中傀顾,則返回此值襟铭,如果沒有找到,則進入實際的MenuService.getMenu方法短曾,在返回調(diào)用結(jié)果給調(diào)用者之前寒砖,還會將此查詢結(jié)果緩存以備下次使用。

集成Spring cache

集成Spring Cache嫉拐,只需要在pom中使用如下依賴

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

如果你使用Spring自帶的內(nèi)存的緩存管理器哩都,需要在appliaction.properties里配置屬性

spring.cache.type=Simple

Simple只適合單機應用或者開發(fā)環(huán)境使用或者是一個小微系統(tǒng),通常你的應用是分布式應用婉徘,Spring Boot 還支持集成更多的緩存服務器漠嵌。

  • simple: 基于ConcurrentHashMap實現(xiàn)的緩存,適合單機或者開發(fā)環(huán)境使用盖呼。

  • none:關(guān)閉緩存儒鹿,比如開發(fā)階段先確保功能正確,可以先禁止使用緩存

  • redis:使用redis作為緩存几晤,你還需要在pom里增加redis依賴约炎。本章緩存將重點介紹redis緩存以及擴展redis實現(xiàn)一二級緩存

  • Generic,用戶自定義緩存實現(xiàn)锌仅,用戶需要實現(xiàn)一個org.springframework.cache.CacheManager的實現(xiàn)

  • 其他還有JCache章钾,EhCache 2.x,Hazelcast等热芹,為了保持本書的簡單,將不在這里一一介紹惨撇。

最后伊脓,需要使用注解 @EnableCaching 打開緩存功能。

@SpringBootApplication
@EnableCaching
public class Ch14Application {
  public static void main(String[] args) {
    SpringApplication.run(Ch14Application.class, args);
  }
}

實現(xiàn)Redis 倆級緩存

SpringBoot自帶的Redis緩存非常容易使用,但由于通過網(wǎng)絡訪問了Redis报腔,效率還是比傳統(tǒng)的跟應用部署在一起的一級緩存略慢株搔。本章中,擴展RedisCacheManager和RedisCache纯蛾,在訪問Redis之前纤房,先訪問一個ConcurrentHashMap實現(xiàn)的簡單一級緩存,如果有緩存項翻诉,則返回給應用炮姨,如果沒有,再從Redis里取碰煌,并將緩存對象放到一級緩存里

當緩存項發(fā)生變化的時候舒岸,注解@CachePut 和 @CacheEvict會觸發(fā)RedisCache的put( Object key, Object value)和evict(Object key)操作,倆級緩存需要同時更新ConcurrentHashMap和Redis緩存芦圾,且需要通過Redis的Pub發(fā)出通知消息蛾派,其他Spring Boot應用通過Sub來接收消息,同步更新Spring Boot應用自身的一級緩存个少。

為了簡單起見洪乍,一級緩并沒有緩存過期策略,用戶系統(tǒng)如果會有大量數(shù)據(jù)需要放到一級緩存夜焦,需要再次擴展這里的代碼典尾,比如使用LRUHashMap代替Map

實現(xiàn) TowLevelCacheManager

首先,創(chuàng)建創(chuàng)建一個新的緩存管理器糊探,命名為TowLevelCacheManager钾埂,繼承了Spring Boot的RedisCacheManager,重載decorateCache方法科平。返回的是我們新創(chuàng)建的LocalAndRedisCache 緩存實現(xiàn)褥紫。

class TowLevelCacheManager extends RedisCacheManager {
    RedisTemplate redisTemplate;
    public TowLevelCacheManager(RedisTemplate redisTemplate,RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter,defaultCacheConfiguration);
        this.redisTemplate = redisTemplate;
    }
    //使用RedisAndLocalCache代替Spring Boot自帶的RedisCache
    @Override
    protected Cache decorateCache(Cache cache) {
        return new RedisAndLocalCache(this, (RedisCache) cache);
    }

  public void publishMessage(String cacheName) {
    this.redisTemplate.convertAndSend(topicName, cacheName);
  }
  // 接受一個消息清空本地緩存
  public void receiver(String name) {
    RedisAndLocalCache cache = ((RedisAndLocalCache) this.getCache(name));
    if(cache!=null){
      cache.clearLocal();
    }
  }

}

在Spring Cache中,在緩存管理器創(chuàng)建好每個緩存后瞪慧,都會調(diào)用decorateCache方法髓考,這樣緩存管理器子類有機會實現(xiàn)自己的擴展,在這段代碼弃酌,返回了自定義的RedisAndLocalCache實現(xiàn)氨菇。 publishMessage方法提供個給Cache,用于當緩存更新的時候妓湘,使用Redis的消息機制通知其他分布式節(jié)點的一級別緩存查蓉。receiver方法對應于publishMessage方法,當收到消息后榜贴,會清空一節(jié)緩存豌研。

創(chuàng)建RedisAndLocalCache

RedisAndLocalCache 是我們系統(tǒng)的核心,他實現(xiàn)了Cache接口,類鹃共,會實現(xiàn)如下操作鬼佣。

  • get操作,通過Key取對應的緩存項霜浴,在調(diào)用父類RedisCache之前晶衷,會先檢測本地緩存是否存在,存在則不需要調(diào)用父類的get操作阴孟。如果不存在晌纫,調(diào)用父類的get操作后,將Redis返回的ValueWrapper放到本地緩存里待下次用温眉。
  • put缸匪,調(diào)用父類put操作更新Redis緩存,同時廣播消息类溢,緩存改變凌蔬。我們將在下一章講如何使用Redis的Pub/Subscribe 來同步緩存
  • evict ,同put操作一樣闯冷,調(diào)用父類處理砂心,清空對應的緩存,同時廣播消息
  • putIfAbsent蛇耀,同put操作一樣辩诞,調(diào)用父類實現(xiàn),同時廣播消息

RedisAndLocalCache 的構(gòu)造如下

class RedisAndLocalCache implements Cache {
  // 本地緩存提供
  ConcurrentHashMap<Object, Object> local = new ConcurrentHashMap<Object, Object>();
  RedisCache redisCache;
  TowLevelCacheManager cacheManager;

  public RedisAndLocalCache(TowLevelCacheManager cacheManager, RedisCache redisCache) {
    this.redisCache = redisCache;
    this.cacheManager = cacheManager;
  }

  @Override
  public String getName() {
    return redisCache.getName();
  }

  @Override
  public Object getNativeCache() {
    return redisCache.getNativeCache();
  }

  //其他get put evict方法參考后面代碼到嗎片段說明
}

如上代碼所示纺涤,RedisAndLocalCache 實現(xiàn)了Cache接口译暂,并使用了真正的RedisCache作為其實現(xiàn)方法。其關(guān)鍵的get和put方法如下

@Override
public ValueWrapper get(Object key) {
  // 一級緩存先取
  ValueWrapper wrapper = (ValueWrapper) local.get(key);
  if (wrapper != null) {
    return wrapper;
  } else {
    // 二級緩存取
    wrapper = redisCache.get(key);
    if (wrapper != null) {
      local.put(key, wrapper);
    }
    return wrapper;
  }
}

@Override
public void put(Object key, Object value) {
  System.out.println(value.getClass().getClassLoader());
  redisCache.put(key, value);
  //通知其他節(jié)點緩存更新
  clearOtherJVM();
}
@Override
public void evict(Object key) {
  redisCache.evict(key);
  //通知其他節(jié)點緩存更新
  clearOtherJVM();
}
protected void clearOtherJVM() {
    cacheManager.publishMessage(redisCache.getName());
}
// 提供給CacheManager清空一節(jié)緩存
public void clearLocal() {
  this.local.clear();
}

變量local代表了一個簡單的緩存實現(xiàn)撩炊, 使用了ConcurrentHashMap外永。其get方法有如下邏輯實現(xiàn)

  • 通過key從本地取出 ValueWrapper
  • 如果ValueWrapper存在,則直接返回
  • 如果ValueWrapper不存在拧咳,則調(diào)用父類RedisCache取得緩存項
  • 如果緩存項為空伯顶,則說明暫時無此項,直接返回空骆膝,等@Cacheable 調(diào)用業(yè)務方法獲取緩存項

put方法實現(xiàn)邏輯如下

  • 先調(diào)用redisCache祭衩,更新二級緩存

  • 調(diào)用clearOtherJVM方法,通知其他節(jié)點緩存更新

  • 其他節(jié)點(包括本節(jié)點)的TowLevelCacheManager收到消息后阅签,會調(diào)用receiver方法從而實現(xiàn)一級緩存

  • 為了簡單起見掐暮,一級緩存的同步更新 僅僅是清空一級緩存而并非采用同步更新緩存項。一級緩存將在下一次get方法調(diào)用時會再次從Reids里加載最新數(shù)據(jù)愉择。

  • 一節(jié)緩存僅僅簡單使用了Map實現(xiàn)劫乱,并未實現(xiàn)緩存的多種策略织中。因此锥涕,如果你的一級緩存如果需要各種緩存策略衷戈,還需要用一些第三方庫或者自行實現(xiàn),但大部分情況下TowLevelCacheManager都足夠使用

緩存同步說明

? 當緩存發(fā)生改變的時候层坠,需要通知分布式系統(tǒng)的TowLevelCacheManager的殖妇,清空一級緩存.這里使用Redis實現(xiàn)消息通知,關(guān)于Redis消息發(fā)布和訂閱破花,參考Redis一章谦趣。

為了實現(xiàn)Redis的Pub/Sub 模式,我們需要在CacheConfig里添加一些代碼座每,創(chuàng)建一個消息監(jiān)聽器

//定義一個redis 的頻道前鹅,默認叫cache,用于pub/sub
@Value("${springext.cache.redis.topic:cache}")
String topicName;
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                        MessageListenerAdapter listenerAdapter) {
  RedisMessageListenerContainer container = new RedisMessageListenerContainer();
  container.setConnectionFactory(connectionFactory);
  container.addMessageListener(listenerAdapter, new PatternTopic(topicName));
  return container;
}

如上所示峭梳,需要配置文件配置 springext.cache.redis.topic舰绘,指定一個頻道的名字,如果沒有配置葱椭,默認的頻道名稱是cache捂寿。

配置一個監(jiān)聽器很簡單,只需要實現(xiàn)MessageListenerAdapter孵运,并注冊到RedisMessageListenerContainer即可秦陋。

MessageListenerAdapter 需要實現(xiàn)onMessage方法,我們只需要獲取消息內(nèi)容治笨,這里是指要清空的緩存名字驳概,然后交給MyRedisCacheManager 來處理即可

@Bean
MessageListenerAdapter listenerAdapter(final TowLevelCacheManager cacheManager) {
  return new MessageListenerAdapter(new MessageListener() {
    public void onMessage(Message message, byte[] pattern) {
      byte[] bs = message.getChannel();
      try {
        //Sub 一個消息,通知緩存管理器旷赖,這里的type就是Cache的名字
        String type = new String(bs, "UTF-8");
        cacheManager.receiver(type);
      } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
        // 不可能出錯顺又,忽略
      }
    }
  });
}

將代碼組合在一起

前三節(jié)分別實現(xiàn)了緩存管理器,緩存杠愧,還有緩存之間的同步待榔,現(xiàn)在需要將緩存管理器配置為應用的緩存管理器,通過搭配@Configuration和@Bean實現(xiàn)

@Configuration
public class CacheConfig {
  @Bean
  public TowLevelCacheManager cacheManager(RedisTemplate redisTemplate) {
    //RedisCache需要一個RedisCacheWriter來實現(xiàn)讀寫Redis
    RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory());
    /*SerializationPair用于Java和Redis之間的序列化和反序列化流济,我們這里使用自帶的JdkSerializationRedisSerializer,并在反序列化過程中锐锣,使用當前的ClassLoader*/
    SerializationPair pair = SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader()));
    /*構(gòu)造一個RedisCache的配置,比如是否使用前綴绳瘟,比如Key和Value的序列化機制(*/
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
    /*創(chuàng)建CacheManager雕憔,并返回給Spring 容器*/
    TowLevelCacheManager cacheManager = new TowLevelCacheManager(redisTemplate,writer,config);
    return cacheManager;
  }
}

構(gòu)造一個TowLevelCacheManager較為復雜,這是因為構(gòu)造RedisCacheManager復雜導致的糖声,構(gòu)造RedisCacheManager需要如下倆個參數(shù)

  • RedisCacheWriter斤彼,一個實現(xiàn)Redis操作的接口分瘦,SpringBoot提供了NoLock和Lock倆種實現(xiàn),在緩存寫操作的時候琉苇,前者有較高性能嘲玫,而后者實現(xiàn)了Redis鎖。
  • RedisCacheConfiguration 用于設置緩存特性并扇,比如緩存項目的TTL(存活時間)去团,緩存Key的前綴等,默認情況是TTL為0穷蛹,不使用前綴土陪。你可以為緩存管理器設置默認的配置,也可以為每一個緩存設置一個配置肴熏。
    最為重要的配置是SerializationPair鬼雀,用于Java和Redis的序列化和反序列化操作,這里我們使用我們這里使用自帶的JdkSerializationRedisSerializer作為序列化機制蛙吏,這個類在Reids一章有詳細介紹源哩。

如上代碼實現(xiàn)了一二級緩存,行數(shù)不到200行代碼出刷。相對于自帶的RedisCache來說璧疗,緩存效率更高。相對于專業(yè)的一二級緩存服務器來說馁龟,如Ehcache+Terracotta組合崩侠,更加輕量級

最后,本博客節(jié)選了我的書 <Spring Boot 2精髓:從構(gòu)建小系統(tǒng)到架構(gòu)分布式大系統(tǒng)>, 此例子可以直接從gitee上下載 https://gitee.com/xiandafu/Spring-Boot-2.0-Samples
歡迎反饋
我的博客即將搬運同步至騰訊云+社區(qū)坷檩,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末却音,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子矢炼,更是在濱河造成了極大的恐慌系瓢,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件句灌,死亡現(xiàn)場離奇詭異夷陋,居然都是意外死亡,警方通過查閱死者的電腦和手機胰锌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門骗绕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人资昧,你說我怎么就攤上這事酬土。” “怎么了格带?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵撤缴,是天一觀的道長刹枉。 經(jīng)常有香客問我,道長屈呕,這世上最難降的妖魔是什么微宝? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮凉袱,結(jié)果婚禮上芥吟,老公的妹妹穿的比我還像新娘侦铜。我一直安慰自己专甩,他們只是感情好,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布钉稍。 她就那樣靜靜地躺著涤躲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贡未。 梳的紋絲不亂的頭發(fā)上种樱,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機與錄音俊卤,去河邊找鬼嫩挤。 笑死,一個胖子當著我的面吹牛消恍,可吹牛的內(nèi)容都是我干的岂昭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼狠怨,長吁一口氣:“原來是場噩夢啊……” “哼约啊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起佣赖,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤恰矩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后憎蛤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體外傅,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年俩檬,在試婚紗的時候發(fā)現(xiàn)自己被綠了萎胰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡豆胸,死狀恐怖奥洼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晚胡,我是刑警寧澤灵奖,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布嚼沿,位于F島的核電站,受9級特大地震影響瓷患,放射性物質(zhì)發(fā)生泄漏骡尽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一擅编、第九天 我趴在偏房一處隱蔽的房頂上張望攀细。 院中可真熱鬧,春花似錦爱态、人聲如沸谭贪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俭识。三九已至,卻和暖如春洞渔,著一層夾襖步出監(jiān)牢的瞬間套媚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工磁椒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留堤瘤,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓浆熔,卻偏偏與公主長得像本辐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蘸拔,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348