緩存系統的用來代替直接訪問數據庫,用來提高系統性能,減少數據庫負載。早期緩存跟系統在一個虛擬機裏,這樣內存訪問,速度最快。 後來應用系統水平擴展,緩存做爲一個獨立系統存在,如redis,可是每次從緩存獲取數據,都仍是要經過網絡訪問才能獲取,效率相對於早先從內存裏獲取,仍是不夠逆天快。若是一個應用,好比傳統的企業應用,一次頁面顯示,要訪問數次redis,那效果就不是特別好,性能不夠快不說,還容易使得Reids負載太高,Redis的主機出現各類物理故障。所以,如今有人提出了一二級緩存。即一級緩存跟系統在一個虛擬機內,這樣速度最快。二級緩存位於redis裏,當一級緩存沒有數據的時候,再從redis裏獲取,並同步到一級緩存裏。這跟CPU的一級緩存,二級緩存是一個道理。固然也面對一樣的問題。html
Cache 一般有以下組件構成java
Spring Boot 自己提供了一個基於ConcurrentHashMap 的緩存機制,也集成了EhCache2.x,JCache(JSR-107,EhCache3.x,Hazelcast,Infinispan),還有Couchbase,Redies等。Spring Boot應用經過註解的方式使用統一的使用緩存,只需在方法上使用緩存註解便可,其緩存的具體實現依賴於你選擇的目標緩存管理器。以下使用@Cacheablegit
[@Service](https://my.oschina.net/service) public class MenuServiceImpl implements MenuService { @Cacheable("menu") public Menu getMenu(Long id) {...} }
MenuService實例做爲一個容器管理bean,Spring將會生成代理類,在實際調用MenuService.getMenu方法前,會調用緩存管理器,取得名"menu"的緩存,此時,緩存的key就是方法參數id,若是緩存命中,則返回此值,若是沒有找到,則進入實際的MenuService.getMenu方法,在返回調用結果給調用者以前,還會將此查詢結果緩存以備下次使用。redis
集成Spring Cache,只須要在pom中使用以下依賴算法
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
若是你使用Spring自帶的內存的緩存管理器,須要在appliaction.properties裏配置屬性spring
spring.cache.type=Simple
Simple只適合單機應用或者開發環境使用或者是一個小微系統,一般你的應用是分佈式應用,Spring Boot 還支持集成更多的緩存服務器。數據庫
simple: 基於ConcurrentHashMap實現的緩存,適合單機或者開發環境使用。緩存
none:關閉緩存,好比開發階段先確保功能正確,能夠先禁止使用緩存服務器
redis:使用redis做爲緩存,你還須要在pom裏增長redis依賴。本章緩存將重點介紹redis緩存以及擴展redis實現一二級緩存網絡
Generic,用戶自定義緩存實現,用戶須要實現一個org.springframework.cache.CacheManager的實現
其餘還有JCache,EhCache 2.x,Hazelcast等,爲了保持本書的簡單,將不在這裏一一介紹。
最後,須要使用註解 @EnableCaching 打開緩存功能。
@SpringBootApplication @EnableCaching public class Ch14Application { public static void main(String[] args) { SpringApplication.run(Ch14Application.class, args); } }
SpringBoot自帶的Redis緩存很是容易使用,但因爲經過網絡訪問了Redis,效率仍是比傳統的跟應用部署在一塊兒的一級緩存略慢。本章中,擴展RedisCacheManager和RedisCache,在訪問Redis以前,先訪問一個ConcurrentHashMap實現的簡單一級緩存,若是有緩存項,則返回給應用,若是沒有,再從Redis裏取,並將緩存對象放到一級緩存裏
當緩存項發生變化的時候,註解@CachePut 和 @CacheEvict會觸發RedisCache的put( Object key, Object value)和evict(Object key)操做,倆級緩存須要同時更新ConcurrentHashMap和Redis緩存,且須要經過Redis的Pub發出通知消息,其餘Spring Boot應用經過Sub來接收消息,同步更新Spring Boot應用自身的一級緩存。
爲了簡單起見,一級緩並無緩存過時策略,用戶系統若是會有大量數據須要放到一級緩存,須要再次擴展這裏的代碼,好比使用LRUHashMap代替Map
首先,建立建立一個新的緩存管理器,命名爲TowLevelCacheManager,繼承了Spring Boot的RedisCacheManager,重載decorateCache方法。返回的是咱們新建立的LocalAndRedisCache 緩存實現。
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中,在緩存管理器建立好每一個緩存後,都會調用decorateCache方法,這樣緩存管理器子類有機會實現本身的擴展,在這段代碼,返回了自定義的RedisAndLocalCache實現。 publishMessage方法提供個給Cache,用於當緩存更新的時候,使用Redis的消息機制通知其餘分佈式節點的一級別緩存。receiver方法對應於publishMessage方法,當收到消息後,會清空一節緩存。
RedisAndLocalCache 是咱們系統的核心,他實現了Cache接口,類,會實現以下操做。
RedisAndLocalCache 的構造以下
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 實現了Cache接口,並使用了真正的RedisCache做爲其實現方法。其關鍵的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); //通知其餘節點緩存更新 clearOtherJVM(); } @Override public void evict(Object key) { redisCache.evict(key); //通知其餘節點緩存更新 clearOtherJVM(); } protected void clearOtherJVM() { cacheManager.publishMessage(redisCache.getName()); } // 提供給CacheManager清空一節緩存 public void clearLocal() { this.local.clear(); }
變量local表明了一個簡單的緩存實現, 使用了ConcurrentHashMap。其get方法有以下邏輯實現
put方法實現邏輯以下
先調用redisCache,更新二級緩存
調用clearOtherJVM方法,通知其餘節點緩存更新
其餘節點(包括本節點)的TowLevelCacheManager收到消息後,會調用receiver方法從而實現一級緩存
爲了簡單起見,一級緩存的同步更新 僅僅是清空一級緩存而並不是採用同步更新緩存項。一級緩存將在下一次get方法調用時會再次從Reids里加載最新數據。
一節緩存僅僅簡單使用了Map實現,並未實現緩存的多種策略。所以,若是你的一級緩存若是須要各類緩存策略,還須要用一些第三方庫或者自行實現,但大部分狀況下TowLevelCacheManager都足夠使用
當緩存發生改變的時候,須要通知分佈式系統的TowLevelCacheManager的,清空一級緩存.這裏使用Redis實現消息通知,關於Redis消息發佈和訂閱,參考Redis一章。
爲了實現Redis的Pub/Sub 模式,咱們須要在CacheConfig裏添加一些代碼,建立一個消息監聽器
//定義一個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。
配置一個監聽器很簡單,只須要實現MessageListenerAdapter,並註冊到RedisMessageListenerContainer便可。
MessageListenerAdapter 須要實現onMessage方法,咱們只須要獲取消息內容,這裏是指要清空的緩存名字,而後交給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(); // 不可能出錯,忽略 } } }); }
前三節分別實現了緩存管理器,緩存,還有緩存之間的同步,如今須要將緩存管理器配置爲應用的緩存管理器,經過搭配@Configuration和@Bean實現
@Configuration public class CacheConfig { @Bean public TowLevelCacheManager cacheManager(RedisTemplate redisTemplate) { //RedisCache須要一個RedisCacheWriter來實現讀寫Redis RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory()); /*SerializationPair用於Java和Redis之間的序列化和反序列化,咱們這裏使用自帶的JdkSerializationRedisSerializer,並在反序列化過程當中,使用當前的ClassLoader*/ SerializationPair pair = SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader())); /*構造一個RedisCache的配置,好比是否使用前綴,好比Key和Value的序列化機制(*/ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair); /*建立CacheManager,並返回給Spring 容器*/ TowLevelCacheManager cacheManager = new TowLevelCacheManager(redisTemplate,writer,config); return cacheManager; } }
構造一個TowLevelCacheManager較爲複雜,這是由於構造RedisCacheManager複雜致使的,構造RedisCacheManager須要以下倆個參數
如上代碼實現了一二級緩存,行數不到200行代碼。相對於自帶的RedisCache來講,緩存效率更高。相對於專業的一二級緩存服務器來講,如Ehcache+Terracotta組合,更加輕量級
最後,本博客節選了個人書 <Spring Boot 2精髓:從構建小系統到架構分佈式大系統>, 此例子能夠直接從gitee上下載 https://gitee.com/xiandafu/Spring-Boot-2.0-Samples 歡迎反饋