1. 程式人生 > 程式設計 >如何擴充套件Spring Cache實現支援多級快取

如何擴充套件Spring Cache實現支援多級快取

為什麼多級快取

快取的引入是現在大部分系統所必須考慮的

redis 作為常用中介軟體,雖然我們一般業務系統(畢竟業務量有限)不會遇到如下圖 在隨著 data-size 的增大和資料結構的複雜的造成效能下降,但網路 IO 消耗會成為整個呼叫鏈路中不可忽視的部分。尤其在 微服務架構中,一次呼叫往往會涉及多次呼叫 例如pig oauth2.0 的 client 認證

如何擴充套件Spring Cache實現支援多級快取

Caffeine 來自未來的本地記憶體快取,效能比如常見的記憶體快取實現效能高出不少詳細對比。

如何擴充套件Spring Cache實現支援多級快取

綜合所述:我們需要構建 L1 Caffeine JVM 級別快取 , L2 Redis 快取。

設計難點

目前大部分應用快取都是基於 Spring Cache 實現,基於註解(annotation)的快取(cache)技術,存在的問題如下:

  • Spring Cache 僅支援 單一的快取來源,即:只能選擇 Redis 實現或者 Caffeine 實現,並不能同時使用。
  • 資料一致性:各層快取之間的資料一致性問題,如應用層快取和分散式快取之前的資料一致性問題。
  • 快取過期:Spring Cache 不支援主動的過期策略

業務流程

如何擴充套件Spring Cache實現支援多級快取

如何使用

引入依賴

<dependency>
  <groupId>com.pig4cloud.plugin</groupId>
  <artifactId>multilevel-cache-spring-boot-starter</artifactId>
  <version>0.0.1</version>
</dependency>

開啟快取支援

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

目標介面宣告 Spring Cache 註解

@Cacheable(value = "get",key = "#key")
@GetMapping("/get")
public String get(String key){
  return "success";
}

效能比較

為保證效能 redis 在 127.0.0.1 環路安裝

  • OS: macOS Mojave
  • CPU: 2.3 GHz Intel Core i5
  • RAM: 8 GB 2133 MHz LPDDR3
  • JVM: corretto_11.jdk

Benchmark Mode Cnt Score Units
多級實現 thrpt 2 2716.074 ops/s
預設 redis thrpt 2 1373.476 ops/s

程式碼原理

自定義 CacheManager 多級快取實現

public class RedisCaffeineCacheManager implements CacheManager {

	@Override
	public Cache getCache(String name) {
		Cache cache = cacheMap.get(name);
		if (cache != null) {
			return cache;
		}
		cache = new RedisCaffeineCache(name,stringKeyRedisTemplate,caffeineCache(),cacheConfigProperties);
		Cache oldCache = cacheMap.putIfAbsent(name,cache);
		log.debug("create cache instance,the cache name is : {}",name);
		return oldCache == null ? cache : oldCache;
	}
}

多級讀取、過期策略實現

public class RedisCaffeineCache extends AbstractValueAdaptingCache {
	protected Object lookup(Object key) {
		Object cacheKey = getKey(key);

  // 1. 先呼叫 caffeine 查詢是否存在指定的值
		Object value = caffeineCache.getIfPresent(key);
		if (value != null) {
			log.debug("get cache from caffeine,the key is : {}",cacheKey);
			return value;
		}

  // 2. 呼叫 redis 查詢在指定的值
		value = stringKeyRedisTemplate.opsForValue().get(cacheKey);

		if (value != null) {
			log.debug("get cache from redis and put in caffeine,cacheKey);
			caffeineCache.put(key,value);
		}
		return value;
	}
}

過期策略,所有更新操作都基於 redis pub/sub 訊息機制更新

public class RedisCaffeineCache extends AbstractValueAdaptingCache {
	@Override
	public void put(Object key,Object value) {
		push(new CacheMessage(this.name,key));
	}

	@Override
	public ValueWrapper putIfAbsent(Object key,Object value) {
				push(new CacheMessage(this.name,key));
	}

	@Override
	public void evict(Object key) {
		push(new CacheMessage(this.name,key));
	}

	@Override
	public void clear() {
		push(new CacheMessage(this.name,null));
	}

	private void push(CacheMessage message) {
		stringKeyRedisTemplate.convertAndSend(topic,message);
	}
}

MessageListener 刪除指定 Caffeine 的指定值

public class CacheMessageListener implements MessageListener {

	private final RedisTemplate<Object,Object> redisTemplate;

	private final RedisCaffeineCacheManager redisCaffeineCacheManager;

	@Override
	public void onMessage(Message message,byte[] pattern) {
		CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
				cacheMessage.getCacheName(),cacheMessage.getKey());
		redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(),cacheMessage.getKey());
	}
}

https://github.com/pig-mesh/multilevel-cache-spring-boot-starter

https://gitee.com/log4j/pig

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。