Redis和Caffeine构建多级缓存

702 阅读3分钟

Caffeine本地缓存

Caffeine是一种高性能的缓存库,是基于Java 8的最佳(最优)缓存框架。

基于Google的Guava Cache,Caffeine提供一个性能卓越的本地缓存(local cache) 实现, 也是SpringBoot内置的本地缓存实现。(Caffeine性能是Guava Cache的6倍)

优点

本地缓存,基于物理机内存,读取速度快

缺点

缓存在多节点之间无法同步,存储容量限制

Redis分布式缓存

Redis缓存是一个开源的使用ANSIC语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

优点

分布式缓存多节点公用,存储容量可扩展

缺点

每次获取缓存都会有网络开销

多级缓存设计

查询:Caffeine作为一级缓存,Redis作为二级缓存,对于指定的高热key,优先访问一级缓存,没有再去访问二级缓存,再没有去执行接口 更新删除:通过redis订阅topic,通知所有节点去删除本地缓存和redis缓存 优点:提高缓存速度,减少网络io 缺点:多节点下缓存的维护与同步成本增加,对于会更新的key,不可靠性加大

实现

新建自定义缓存

自定义cache,继承AbstractValueAdaptingCache,内部维护Caffeine缓存,通过标识字段usedCaffeineCache来确定当前缓存是否使用Caffeine缓存,usedCaffeineCache字段通过@CacheAble中name字段获取

/**
	 * 获取缓存
	 * @param key
	 * @return
	 */
	@Override
	protected Object lookup(Object key) {
		Object cacheKey = getKey(key);
		Object value;
		if (usedCaffeineCache) {
			value = getCaffeineValue(key);
			if (value != null) {
				if (log.isDebugEnabled()){
					log.debug("get cache from caffeine, the key is : {}", cacheKey);
				}
				return value;
			}
		}

		value = getRedisValue(key);

		if (value != null) {
			if (log.isDebugEnabled()) {
				log.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
			}
			setCaffeineValue(key, value);
		}
		return value;
	}
	
	/**
	 * 清除缓存
	 * @param key
	 */
	@Override
	public void evict(Object key) {
		// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
		stringKeyRedisTemplate.delete(getKey(key));

		if (this.usedCaffeineCache) {
			push(new CacheMessage(this.name, key));

			caffeineCache.invalidate(key);
		}
	}
	
	/**
	 * 添加缓存
	 * @param key
	 * @param value
	 */
	@Override
	public void put(Object key, Object value) {
		if (!super.isAllowNullValues() && value == null) {
			this.evict(key);
			return;
		}
		doPut(key, value);
	}
	
	private void doPut(Object key, Object value) {
		value = toStoreValue(value);
		Duration expire = getExpire();
		setRedisValue(key, value, expire);

		if (this.usedCaffeineCache) {
			push(new CacheMessage(this.name, key));

			setCaffeineValue(key, value);
		}
	}

自定义cacheManage,在本地维护一个CacheMap

private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
	@Override
	public Cache getCache(String name) {
		Cache cache = cacheMap.get(name);
		if (cache != null) {
			return cache;
		}
		if (!dynamic && !cacheNames.contains(name)) {
			return null;
		}

		cache = cacheFactory.createCache(name);
		Cache oldCache = cacheMap.putIfAbsent(name, cache);
		log.debug("create cache instance, the cache name is : {}", name);
		return oldCache == null ? cache : oldCache;
	}

新建DefaultCacheFactory来创建自定义的缓存,通过解析name去判定是否允许使用Caffeine一级缓存,默认不使用

  /**
   * 创建缓存
   * @param name
   * @return
   */
  @Override
  public RedisCaffeineCache createCache(String name) {
    boolean usedCaffeineCache = false;
    String[] redisConfigs = RedisUtil.replaceName(name, cacheConfigProperties.getRedis().getDelimiter());
    if (redisConfigs.length>2){
      String cacheType = redisConfigs[2];
      if (!StrUtil.isBlank(cacheType) && CacheType.RedisCaffeineCache.getCacheType().equals(cacheType)){
        usedCaffeineCache = true;
      }
    }
    return new RedisCaffeineCache(name, stringKeyRedisTemplate, cacheConfigProperties,usedCaffeineCache);
  }

注入自定义CacheManage

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnBean(RedisTemplate.class)
	public RedisCaffeineCacheManager cacheManager(CacheConfigProperties cacheConfigProperties,
			RedisTemplate<String, Object> redisTemplate,
			ObjectProvider<RedisCaffeineCacheManagerCustomizer> cacheManagerCustomizers,
			CacheFactory cacheFactory) {
		RedisCaffeineCacheManager cacheManager = new RedisCaffeineCacheManager(cacheConfigProperties, redisTemplate,
				cacheFactory);
		cacheManagerCustomizers.orderedStream().forEach(customizer -> customizer.customize(cacheManager));
		return cacheManager;
	}

使用

name规则:name+过期时间+是否使用一级缓存 不使用一级缓存

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

每次都会去访问redis获得缓存

1667978526.697602 [0 127.0.0.1:50098] "GET" "get#6m:test"
1667978526.898507 [0 127.0.0.1:50099] "GET" "get#6m:test"
1667978527.093476 [0 127.0.0.1:50100] "GET" "get#6m:test"
1667978527.307849 [0 127.0.0.1:50101] "GET" "get#6m:test"
1667978527.496456 [0 127.0.0.1:50102] "GET" "get#6m:test"
1667978527.728409 [0 127.0.0.1:50103] "GET" "get#6m:test"
1667978527.933313 [0 127.0.0.1:50104] "GET" "get#6m:test"
1667978528.132622 [0 127.0.0.1:50106] "GET" "get#6m:test"
1667978528.379935 [0 127.0.0.1:50105] "GET" "get#6m:test"

使用一级缓存

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

只会访问一次redis,其余都是获取本地缓存

1667978655.462160 [0 127.0.0.1:50585] "GET" "get#6m#1:test"
1667978655.469164 [0 127.0.0.1:50588] "SETEX" "get#6m#1:test" "360" "\"test\""
1667978655.474989 [0 127.0.0.1:50587] "PUBLISH" "cache:redis:caffeine:topic" "{\"@class\":\"com.jwk.common.redis.support.CacheMessage\",\"cacheName\":\"get#6m#1\",\"key\":\"test\"}"
1667978655.744241 [0 127.0.0.1:50589] "GET" "get#6m#1:test"

详细代码地址:gitee.com/musi1996/jc…