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

緩存系統的用來代替直接訪問數據庫,用來提升系統性能,減小數據庫負載。早期緩存跟系統在一個虛擬機裏,這樣內存訪問,速度最快。 後來應用系統水平擴展,緩存作爲一個獨立系統存在,如redis,但是每次從緩存獲取數據,都還是要通過網絡訪問才能獲取,效率相對於早先從內存裏獲取,還是不夠逆天快。如果一個應用,比如傳統的企業應用,一次頁面顯示,要訪問數次redis,那效果就不是特別好,性能不夠快不說,還容易使得Reids負載過高,Redis的主機出現各種物理故障。因此,現在有人提出了一二級緩存。即一級緩存跟系統在一個虛擬機內,這樣速度最快。二級緩存位於redis裏,當一級緩存沒有數據的時候,再從redis裏獲取,並同步到一級緩存裏。這跟CPU的一級緩存,二級緩存是一個道理。當然也面對同樣的問題。

緩存概念

Cache 通常有如下組件構成

  • CacheManager,用來創建,管理,管理多個命名唯一的Cache。如可以有組織機構緩存,菜單項的緩存,菜單樹的緩存等
  • Cache類似Map那樣的Key—Value存儲結構,Value部分 通常包含了緩存的對象,通過Key來取得緩存對象
  • 緩存項,存放在緩存裏的對象,常常需要實現序列化接口,以支持分佈式緩存。
  • Cache存儲方式,緩存組件的可以將對象放到內存,也可以是其他緩存服務器,Spring Boot 提供了一個基於ConcurrentMap的緩存,同時也集成了Redis,EhCache 2.x,JCache緩存服務器等
  • 緩存策略,通常Cache 還可以有不同的緩存策略,如設置緩存最大的容量,緩存項的過期時間等
  • 分佈式緩存,緩存通常按照緩存數據類型存放在不同緩存服務器上,或者同一類型的緩存,按照某種算法,不同key的數據放在不同的緩存服務器上。
  • Cache Hit,當從Cache中取得期望的緩存項,我們通常稱之爲緩存命中。如果沒有命中我們稱之爲Cache Miss,意味着需要從數據來源處重新取出並放回Cache中
  • Cache Miss:緩存丟失,根據Key沒有從緩存中找到對應的緩存項
  • Cache Evication:緩存清除操作。
  • Hot Data,熱點數據,緩存系統能調整算法或者內部存儲方式,使得將最有可能頻繁訪問的數據能儘快訪問到。
  • On-Heap,Java分配對象都是在堆內存裏,有最快的獲取速度。由於虛擬機的垃圾回收管理,緩存放過多的對象會導致垃圾回收時間過長,從而有可能影響性能。
  • Off-Heap,堆外內存,對象存放到在虛擬機分配的堆外內存,因此不受垃圾回收管理的管理,不影響系統系統,但堆外內存的對象要被使用,還要序列化成堆內對象。很多緩存工具會把不常用的對象放到堆外,把熱點數據放到堆內。

Spring Boot 緩存

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

    [@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方法,在返回調用結果給調用者之前,還會將此查詢結果緩存以備下次使用。

集成Spring cache

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

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

如果你使用Spring自帶的內存的緩存管理器,需要在appliaction.properties裏配置屬性

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);
  }
}

實現Redis 倆級緩存

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

首先,創建創建一個新的緩存管理器,命名爲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

RedisAndLocalCache 是我們系統的核心,他實現了Cache接口,類,會實現如下操作。

  • get操作,通過Key取對應的緩存項,在調用父類RedisCache之前,會先檢測本地緩存是否存在,存在則不需要調用父類的get操作。如果不存在,調用父類的get操作後,將Redis返回的ValueWrapper放到本地緩存裏待下次用。
  • put,調用父類put操作更新Redis緩存,同時廣播消息,緩存改變。我們將在下一章講如何使用Redis的Pub/Subscribe 來同步緩存
  • evict ,同put操作一樣,調用父類處理,清空對應的緩存,同時廣播消息
  • putIfAbsent,同put操作一樣,調用父類實現,同時廣播消息

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方法有如下邏輯實現

  • 通過key從本地取出 ValueWrapper
  • 如果ValueWrapper存在,則直接返回
  • 如果ValueWrapper不存在,則調用父類RedisCache取得緩存項
  • 如果緩存項爲空,則說明暫時無此項,直接返回空,等@Cacheable 調用業務方法獲取緩存項

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需要如下倆個參數

  • RedisCacheWriter,一個實現Redis操作的接口,SpringBoot提供了NoLock和Lock倆種實現,在緩存寫操作的時候,前者有較高性能,而後者實現了Redis鎖。
  • RedisCacheConfiguration 用於設置緩存特性,比如緩存項目的TTL(存活時間),緩存Key的前綴等,默認情況是TTL爲0,不使用前綴。你可以爲緩存管理器設置默認的配置,也可以爲每一個緩存設置一個配置。 最爲重要的配置是SerializationPair,用於Java和Redis的序列化和反序列化操作,這裏我們使用我們這裏使用自帶的JdkSerializationRedisSerializer作爲序列化機制,這個類在Reids一章有詳細介紹。

如上代碼實現了一二級緩存,行數不到200行代碼。相對於自帶的RedisCache來說,緩存效率更高。相對於專業的一二級緩存服務器來說,如Ehcache+Terracotta組合,更加輕量級

最後,本博客節選了我的書 <Spring Boot 2精髓:從構建小系統到架構分佈式大系統>, 此例子可以直接從gitee上下載 https://gitee.com/xiandafu/Spring-Boot-2.0-Samples 歡迎反饋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章