spring boot + spring cache 实现两级缓存(redis + caffeine)

原创
2018/01/31 16:12
阅读数 11.9W

spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis、Caffeine、JCache、EhCache等等。但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存)。在很多场景下,可以结合起来实现一、二级缓存的方式,能够很大程度提高应用的处理效率。

内容说明:

  • 缓存、两级缓存
  • spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
  • spring boot + spring cache:RedisCache实现中的缺陷
  • caffeine简介
  • spring boot + spring cache 实现两级缓存(redis + caffeine)

缓存、两级缓存


简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了应用内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存

spring cache


当使用缓存的时候,一般是如下的流程:

使用缓存的一般流程

从流程图中可以看出,为了使用缓存,在原有业务处理的基础上,增加了很多对于缓存的操作,如果将这些耦合到业务代码当中,开发起来就有很多重复性的工作,并且不太利于根据代码去理解业务。

spring cache是spring-context包中提供的基于注解方式使用的缓存组件,定义了一些标准接口,通过实现这些接口,就可以通过在方法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在一起的问题。spring cache的实现是使用spring aop中对方法切面(MethodInterceptor)封装的扩展,当然spring aop也是基于Aspect来实现的。

spring cache核心的接口就两个:Cache和CacheManager

spring cache包结构

Cache接口

提供缓存的具体操作,比如缓存的放入、读取、清理,spring框架中默认提供的实现有:

spring框架中默认实现的Cache

除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中

spring-context-support包中的Cache接口实现类

#Cache.java

package org.springframework.cache;

import java.util.concurrent.Callable;

public interface Cache {

	// cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheName
	String getName();

	// 获取实际使用的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。暂时没发现实际用处,可能只是提供获取原生缓存的bean,以便需要扩展一些缓存操作或统计之类的东西
	Object getNativeCache();

	// 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值
	ValueWrapper get(Object key);

	// 通过key获取缓存值,返回的是实际值,即方法的返回值类型
	<T> T get(Object key, Class<T> type);

	// 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库
	<T> T get(Object key, Callable<T> valueLoader);

	// 将@Cacheable注解方法返回的数据放入缓存中
	void put(Object key, Object value);

	// 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据
	ValueWrapper putIfAbsent(Object key, Object value);

	// 删除缓存
	void evict(Object key);

	// 删除缓存中的所有数据。需要注意的是,具体实现中只删除使用@Cacheable注解缓存的所有数据,不要影响应用内的其他缓存
	void clear();

	// 缓存返回值的包装
	interface ValueWrapper {

		// 返回实际缓存的对象
		Object get();
	}

	// 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出
	@SuppressWarnings("serial")
	class ValueRetrievalException extends RuntimeException {

		private final Object key;

		public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) {
			super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
			this.key = key;
		}

		public Object getKey() {
			return this.key;
		}
	}
}

CacheManager接口

主要提供Cache实现bean的创建,每个应用里可以通过cacheName来对Cache进行隔离,每个cacheName对应一个Cache实现。spring框架中默认提供的实现与Cache的实现都是成对出现,包结构也在上图中

#CacheManager.java

package org.springframework.cache;

import java.util.Collection;

public interface CacheManager {

	// 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况
	Cache getCache(String name);

	// 返回所有的cacheName
	Collection<String> getCacheNames();
}

常用注解说明

  • @Cacheable:主要应用到查询数据的方法上
package org.springframework.cache.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Callable;

import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

        // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean
	@AliasFor("cacheNames")
	String[] value() default {};

	@AliasFor("value")
	String[] cacheNames() default {};

        // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)
	String key() default "";

	// 缓存key生成器,默认实现是SimpleKeyGenerator
	String keyGenerator() default "";

	// 指定使用哪个CacheManager
	String cacheManager() default "";

	// 缓存解析器
	String cacheResolver() default "";

	// 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断
	String condition() default "";
        
        // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断
	String unless() default "";

	// 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法
	boolean sync() default false;

}
  • @CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性
package org.springframework.cache.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {

        // ...相同属性说明请参考@Cacheable中的说明

	// 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法
	boolean allEntries() default false;

	// 调用方法之前或之后清除缓存
	boolean beforeInvocation() default false;
}
  • @CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable

  • @Caching:用于在一个方法上配置多种注解

  • @EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效

spring boot + spring cache


spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使用时只需要配置使用哪个缓存(enum CacheType)即可。

spring boot autoconfigure中的缓存配置

spring boot中多增加了一个可以扩展的东西,就是CacheManagerCustomizer接口,可以自定义实现这个接口,然后对CacheManager做一些设置,比如:

package com.itopener.demo.cache.redis.config;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.data.redis.cache.RedisCacheManager;

public class RedisCacheManagerCustomizer implements CacheManagerCustomizer<RedisCacheManager> {

	@Override
	public void customize(RedisCacheManager cacheManager) {
		// 默认过期时间,单位秒
		cacheManager.setDefaultExpiration(1000);
		cacheManager.setUsePrefix(false);
		Map<String, Long> expires = new ConcurrentHashMap<String, Long>();
		expires.put("userIdCache", 2000L);
		cacheManager.setExpires(expires);
	}

}

加载这个bean:

package com.itopener.demo.cache.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author fuwei.deng
 * @date 2017年12月22日 上午10:24:54
 * @version 1.0.0
 */
@Configuration
public class CacheRedisConfiguration {
	
	@Bean
	public RedisCacheManagerCustomizer redisCacheManagerCustomizer() {
		return new RedisCacheManagerCustomizer();
	}
}

常用的缓存就是Redis了,Redis对于spring cache接口的实现是在spring-data-redis包中

spring-data-redis中spring cache的实现

这里提下我认为的RedisCache实现中的缺陷:

1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:

  • 判断缓存key是否存在
  • 如果key存在,再获取缓存数据,并返回

因此当判断key存在后缓存失效了,再去获取缓存是没有数据的,就返回null了。

2.RedisCacheManager中是否允许存储空值的属性(cacheNullValues)默认为false,即不允许存储空值,这样会存在缓存穿透的风险。缺陷是这个属性是final类型的,只能在创建对象是通过构造方法传入,所以要避免缓存穿透就只能自己在应用内声明RedisCacheManager这个bean了

3.RedisCacheManager中的属性无法通过配置文件直接配置,只能在应用内实现CacheManagerCustomizer接口来进行设置,个人认为不太方便

Caffeine


Caffeine是一个基于Google开源的Guava设计理念的一个高性能内存缓存,使用java8开发,spring boot引入Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeine

caffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很大帮助

caffeine的介绍可以参考:http://www.cnblogs.com/oopsguy/p/7731659.html

这里简单说下caffeine基于时间的回收策略有以下几种:

  • expireAfterAccess:访问后到期,从上次读或写发生后的过期时间
  • expireAfterWrite:写入后到期,从上次写入发生之后的过期时间
  • 自定义策略:到期时间由实现Expiry接口后单独计算

spring boot + spring cache 实现两级缓存(redis + caffeine)


本人开头提到了,就算是使用了redis缓存,也会存在一定程度的网络传输上的消耗,在实际应用当中,会存在一些变更频率非常低的数据,就可以直接缓存在应用内部,对于一些实时性要求不太高的数据,也可以在应用内部缓存一定时间,减少对redis的访问,提高响应速度

由于spring-data-redis框架中redis对spring cache的实现有一些不足,在使用起来可能会出现一些问题,所以就不基于原来的实现去扩展了,直接参考实现方式,去实现Cache和CacheManager接口

还需要注意一点,一般应用都部署了多个节点,一级缓存是在应用内的缓存,所以当对数据更新和清除时,需要通知所有节点进行清理缓存的操作。可以有多种方式来实现这种效果,比如:zookeeper、MQ等,但是既然用了redis缓存,redis本身是有支持订阅/发布功能的,所以就不依赖其他组件了,直接使用redis的通道来通知其他节点进行清理缓存的操作

以下就是对spring boot + spring cache实现两级缓存(redis + caffeine)的starter封装步骤和源码

  • 定义properties配置属性类
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**  
 * @author fuwei.deng
 * @date 2018年1月29日 上午11:32:15
 * @version 1.0.0
 */
@ConfigurationProperties(prefix = "spring.cache.multi")
public class CacheRedisCaffeineProperties {
	
	private Set<String> cacheNames = new HashSet<>();
	
	/** 是否存储空值,默认true,防止缓存穿透*/
	private boolean cacheNullValues = true;
	
	/** 是否动态根据cacheName创建Cache的实现,默认true*/
	private boolean dynamic = true;
	
	/** 缓存key的前缀*/
	private String cachePrefix;
	
	private Redis redis = new Redis();
	
	private Caffeine caffeine = new Caffeine();

	public class Redis {
		
		/** 全局过期时间,单位毫秒,默认不过期*/
		private long defaultExpiration = 0;
		
		/** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高*/
		private Map<String, Long> expires = new HashMap<>();
		
		/** 缓存更新时通知其他节点的topic名称*/
		private String topic = "cache:redis:caffeine:topic";

		public long getDefaultExpiration() {
			return defaultExpiration;
		}

		public void setDefaultExpiration(long defaultExpiration) {
			this.defaultExpiration = defaultExpiration;
		}

		public Map<String, Long> getExpires() {
			return expires;
		}

		public void setExpires(Map<String, Long> expires) {
			this.expires = expires;
		}

		public String getTopic() {
			return topic;
		}

		public void setTopic(String topic) {
			this.topic = topic;
		}
		
	}
	
	public class Caffeine {
		
		/** 访问后过期时间,单位毫秒*/
		private long expireAfterAccess;
		
		/** 写入后过期时间,单位毫秒*/
		private long expireAfterWrite;
		
		/** 写入后刷新时间,单位毫秒*/
		private long refreshAfterWrite;
		
		/** 初始化大小*/
		private int initialCapacity;
		
		/** 最大缓存对象个数,超过此数量时之前放入的缓存将失效*/
		private long maximumSize;
		
		/** 由于权重需要缓存对象来提供,对于使用spring cache这种场景不是很适合,所以暂不支持配置*/
//		private long maximumWeight;
		
		public long getExpireAfterAccess() {
			return expireAfterAccess;
		}

		public void setExpireAfterAccess(long expireAfterAccess) {
			this.expireAfterAccess = expireAfterAccess;
		}

		public long getExpireAfterWrite() {
			return expireAfterWrite;
		}

		public void setExpireAfterWrite(long expireAfterWrite) {
			this.expireAfterWrite = expireAfterWrite;
		}

		public long getRefreshAfterWrite() {
			return refreshAfterWrite;
		}

		public void setRefreshAfterWrite(long refreshAfterWrite) {
			this.refreshAfterWrite = refreshAfterWrite;
		}

		public int getInitialCapacity() {
			return initialCapacity;
		}

		public void setInitialCapacity(int initialCapacity) {
			this.initialCapacity = initialCapacity;
		}

		public long getMaximumSize() {
			return maximumSize;
		}

		public void setMaximumSize(long maximumSize) {
			this.maximumSize = maximumSize;
		}
	}

	public Set<String> getCacheNames() {
		return cacheNames;
	}

	public void setCacheNames(Set<String> cacheNames) {
		this.cacheNames = cacheNames;
	}

	public boolean isCacheNullValues() {
		return cacheNullValues;
	}

	public void setCacheNullValues(boolean cacheNullValues) {
		this.cacheNullValues = cacheNullValues;
	}

	public boolean isDynamic() {
		return dynamic;
	}

	public void setDynamic(boolean dynamic) {
		this.dynamic = dynamic;
	}

	public String getCachePrefix() {
		return cachePrefix;
	}

	public void setCachePrefix(String cachePrefix) {
		this.cachePrefix = cachePrefix;
	}

	public Redis getRedis() {
		return redis;
	}

	public void setRedis(Redis redis) {
		this.redis = redis;
	}

	public Caffeine getCaffeine() {
		return caffeine;
	}

	public void setCaffeine(Caffeine caffeine) {
		this.caffeine = caffeine;
	}

}
  • spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;

import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;

import com.github.benmanes.caffeine.cache.Cache;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:24:11
 * @version 1.0.0
 */
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
	
	private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);

	private String name;
	
	private RedisTemplate<Object, Object> redisTemplate;

	private Cache<Object, Object> caffeineCache;

	private String cachePrefix;

	private long defaultExpiration = 0;

	private Map<String, Long> expires;
	
	private String topic = "cache:redis:caffeine:topic";
	
	protected RedisCaffeineCache(boolean allowNullValues) {
		super(allowNullValues);
	}
	
	public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
		super(cacheRedisCaffeineProperties.isCacheNullValues());
		this.name = name;
		this.redisTemplate = redisTemplate;
		this.caffeineCache = caffeineCache;
		this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
		this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
		this.expires = cacheRedisCaffeineProperties.getRedis().getExpires();
		this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
	}

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

	@Override
	public Object getNativeCache() {
		return this;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T get(Object key, Callable<T> valueLoader) {
		Object value = lookup(key);
		if(value != null) {
			return (T) value;
		}
		
		ReentrantLock lock = new ReentrantLock();
		try {
			lock.lock();
			value = lookup(key);
			if(value != null) {
				return (T) value;
			}
			value = valueLoader.call();
			Object storeValue = toStoreValue(valueLoader.call());
			put(key, storeValue);
			return (T) value;
		} catch (Exception e) {
			try {
                Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
                Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
                RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
                throw exception;                
            } catch (Exception e1) {
                throw new IllegalStateException(e1);
            }
		} finally {
			lock.unlock();
		}
	}

	@Override
	public void put(Object key, Object value) {
		if (!super.isAllowNullValues() && value == null) {
			this.evict(key);
            return;
        }
		long expire = getExpire();
		if(expire > 0) {
			redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
		} else {
			redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
		}
		
		push(new CacheMessage(this.name, key));
		
		caffeineCache.put(key, value);
	}

	@Override
	public ValueWrapper putIfAbsent(Object key, Object value) {
		Object cacheKey = getKey(key);
		Object prevValue = null;
		// 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作
		synchronized (key) {
			prevValue = redisTemplate.opsForValue().get(cacheKey);
			if(prevValue == null) {
				long expire = getExpire();
				if(expire > 0) {
					redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
				} else {
					redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
				}
				
				push(new CacheMessage(this.name, key));
				
				caffeineCache.put(key, toStoreValue(value));
			}
		}
		return toValueWrapper(prevValue);
	}

	@Override
	public void evict(Object key) {
		// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
		redisTemplate.delete(getKey(key));
		
		push(new CacheMessage(this.name, key));
		
		caffeineCache.invalidate(key);
	}

	@Override
	public void clear() {
		// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
		Set<Object> keys = redisTemplate.keys(this.name.concat(":"));
		for(Object key : keys) {
			redisTemplate.delete(key);
		}
		
		push(new CacheMessage(this.name, null));
		
		caffeineCache.invalidateAll();
	}

	@Override
	protected Object lookup(Object key) {
		Object cacheKey = getKey(key);
		Object value = caffeineCache.getIfPresent(key);
		if(value != null) {
			logger.debug("get cache from caffeine, the key is : {}", cacheKey);
			return value;
		}
		
		value = redisTemplate.opsForValue().get(cacheKey);
		
		if(value != null) {
			logger.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
			caffeineCache.put(key, value);
		}
		return value;
	}

	private Object getKey(Object key) {
		return this.name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
	}
	
	private long getExpire() {
		long expire = defaultExpiration;
		Long cacheNameExpire = expires.get(this.name);
		return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
	}
	
	/**
	 * @description 缓存变更时通知其他节点清理本地缓存
	 * @author fuwei.deng
	 * @date 2018年1月31日 下午3:20:28
	 * @version 1.0.0
	 * @param message
	 */
	private void push(CacheMessage message) {
		redisTemplate.convertAndSend(topic, message);
	}
	
	/**
	 * @description 清理本地缓存
	 * @author fuwei.deng
	 * @date 2018年1月31日 下午3:15:39
	 * @version 1.0.0
	 * @param key
	 */
	public void clearLocal(Object key) {
		logger.debug("clear local cache, the key is : {}", key);
		if(key == null) {
			caffeineCache.invalidateAll();
		} else {
			caffeineCache.invalidate(key);
		}
	}
}
  • 实现CacheManager接口
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:24:52
 * @version 1.0.0
 */
public class RedisCaffeineCacheManager implements CacheManager {
	
	private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);
	
	private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
	
	private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
	
	private RedisTemplate<Object, Object> redisTemplate;

	private boolean dynamic = true;

	private Set<String> cacheNames;

	public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
			RedisTemplate<Object, Object> redisTemplate) {
		super();
		this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
		this.redisTemplate = redisTemplate;
		this.dynamic = cacheRedisCaffeineProperties.isDynamic();
		this.cacheNames = cacheRedisCaffeineProperties.getCacheNames();
	}

	@Override
	public Cache getCache(String name) {
		Cache cache = cacheMap.get(name);
		if(cache != null) {
			return cache;
		}
		if(!dynamic && !cacheNames.contains(name)) {
			return cache;
		}
		
		cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties);
		Cache oldCache = cacheMap.putIfAbsent(name, cache);
		logger.debug("create cache instance, the cache name is : {}", name);
		return oldCache == null ? cache : oldCache;
	}
	
	public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(){
		Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
		if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) {
			cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS);
		}
		if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) {
			cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.MILLISECONDS);
		}
		if(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) {
			cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity());
		}
		if(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) {
			cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize());
		}
		if(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) {
			cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS);
		}
		return cacheBuilder.build();
	}

	@Override
	public Collection<String> getCacheNames() {
		return this.cacheNames;
	}
	
	public void clearLocal(String cacheName, Object key) {
		Cache cache = cacheMap.get(cacheName);
		if(cache == null) {
			return ;
		}
		
		RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
		redisCaffeineCache.clearLocal(key);
	}
}
  • redis消息发布/订阅,传输的消息类
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;

import java.io.Serializable;

/**  
 * @author fuwei.deng
 * @date 2018年1月29日 下午1:31:17
 * @version 1.0.0
 */
public class CacheMessage implements Serializable {

	/** */
	private static final long serialVersionUID = 5987219310442078193L;

	private String cacheName;
	
	private Object key;

	public CacheMessage(String cacheName, Object key) {
		super();
		this.cacheName = cacheName;
		this.key = key;
	}

	public String getCacheName() {
		return cacheName;
	}

	public void setCacheName(String cacheName) {
		this.cacheName = cacheName;
	}

	public Object getKey() {
		return key;
	}

	public void setKey(Object key) {
		this.key = key;
	}
	
}
  • 监听redis消息需要实现MessageListener接口
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;

/**  
 * @author fuwei.deng
 * @date 2018年1月30日 下午5:22:33
 * @version 1.0.0
 */
public class CacheMessageListener implements MessageListener {
	
	private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);

	private RedisTemplate<Object, Object> redisTemplate;
	
	private RedisCaffeineCacheManager redisCaffeineCacheManager;

	public CacheMessageListener(RedisTemplate<Object, Object> redisTemplate,
			RedisCaffeineCacheManager redisCaffeineCacheManager) {
		super();
		this.redisTemplate = redisTemplate;
		this.redisCaffeineCacheManager = redisCaffeineCacheManager;
	}

	@Override
	public void onMessage(Message message, byte[] pattern) {
		CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
		logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
		redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
	}

}
  • 增加spring boot配置类
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager;

/**  
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:23:03
 * @version 1.0.0
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
public class CacheRedisCaffeineAutoConfiguration {
	
	@Autowired
	private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
	
	@Bean
	@ConditionalOnBean(RedisTemplate.class)
	public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
		return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
	}
	
	@Bean
	public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> redisTemplate, 
			RedisCaffeineCacheManager redisCaffeineCacheManager) {
		RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
		redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
		CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);
		redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic()));
		return redisMessageListenerContainer;
	}
}
  • 在resources/META-INF/spring.factories文件中增加spring boot配置扫描
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration
  • 接下来就可以使用maven引入使用了
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>
  • 在启动类上增加@EnableCaching注解,在需要缓存的方法上增加@Cacheable注解
package com.itopener.demo.cache.redis.caffeine.service;

import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.itopener.demo.cache.redis.caffeine.vo.UserVO;
import com.itopener.utils.TimestampUtil;

@Service
public class CacheRedisCaffeineService {
	
	private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class);

	@Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
	public UserVO get(long id) {
		logger.info("get by id from db");
		UserVO user = new UserVO();
		user.setId(id);
		user.setName("name" + id);
		user.setCreateTime(TimestampUtil.current());
		return user;
	}
	
	@Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager")
	public UserVO get(String name) {
		logger.info("get by name from db");
		UserVO user = new UserVO();
		user.setId(new Random().nextLong());
		user.setName(name);
		user.setCreateTime(TimestampUtil.current());
		return user;
	}
	
	@CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager")
	public UserVO update(UserVO userVO) {
		logger.info("update to db");
		userVO.setCreateTime(TimestampUtil.current());
		return userVO;
	}
	
	@CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
	public void delete(long id) {
		logger.info("delete from db");
	}
}
  • properties文件中redis的配置跟使用redis是一样的,可以增加两级缓存的配置
#两级缓存的配置
spring.cache.multi.caffeine.expireAfterAccess=5000
spring.cache.multi.redis.defaultExpiration=60000

#spring cache配置
spring.cache.cache-names=userIdCache,userNameCache

#redis配置
#spring.redis.timeout=10000
#spring.redis.password=redispwd
#redis pool
#spring.redis.pool.maxIdle=10
#spring.redis.pool.minIdle=2
#spring.redis.pool.maxActive=10
#spring.redis.pool.maxWait=3000
#redis cluster
spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
spring.redis.cluster.maxRedirects=3

扩展


  • 个人认为redisson的封装更方便一些

    • 对于spring cache缓存的实现没有那么多的缺陷
    • 使用redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更方便
    • 如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应方法增加一级缓存的操作即可
    • 如果有使用分布式锁的情况就更方便了,可以直接使用Redisson中封装的分布式锁
    • redisson中的发布订阅封装得更好用
  • 后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化

源码

https://gitee.com/itopener/springboot

  • starter目录:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent
  • 示例代码目录: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine

参考资料


展开阅读全文
打赏
22
93 收藏
分享
加载中
@SuppressWarnings("unchecked")
  @Override
  public <T> T get(Object key, Callable<T> valueLoader) {
    Object value = lookup(key);
    if(value != null) {
      return (T) value;
    }
    
    ReentrantLock lock = new ReentrantLock();
    try {
      lock.lock();
      value = lookup(key);
      if(value != null) {
        return (T) value;
      }
      value = valueLoader.call();
      Object storeValue = toStoreValue(valueLoader.call());
      put(key, storeValue);
      return (T) value;
    } catch (Exception e) {
      try {
Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
throw exception;
} catch (Exception e1) {
throw new IllegalStateException(e1);
这里的重入锁意义何在?不应该定义为全局变量?
08/24 10:22
回复
举报
你好。我今天试的时候有这样的一个问题。如果我存入缓存的是个null。我下次再去取的时候就会一直报错。
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
if (value != null) { // 当value是NullValue类型的时候,这个判断会进行。然后就会出现转换错误。。
return (T) value;
}
这个地方就会出现转换异常。因为null存到缓存中是nullValue。 再次取的时候就会出现转换异常。
2019/08/26 14:06
回复
举报
楼主 好!
要是同时支持 ehcache,caffeine等多个就好了 😄
Cacheable sync()
2018/09/28 10:33
回复
举报

引用来自“李嘉图”的评论

写的很认真,不过这些都是基于单机应用吧

引用来自“J猿”的评论

看样子你看得不认真😂

引用来自“李嘉图”的评论

假设A机器的二级缓存更新了,并且同时更新了一级缓存,但是B机器已经有了二级缓存,它直接用了,那么此时B的缓存不久不同步了,只有A机器的缓存是同步的

引用来自“Mr---D”的评论

仔细看,有用redis的订阅发布功能进行通知的
缓存那块获取的时候两次查询是有问题的,如果两个线程去取,一个查询缓存为空,一个已经从数据库取得数据,会有多次读取db的问题
2018/06/11 14:51
回复
举报
Mr---D博主

引用来自“可爱猪猪2018”的评论

大哥启动报错啊。。。available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
代码和示例都在码云上:https://gitee.com/itopener/springboot
2018/04/13 15:20
回复
举报

引用来自“丶璀璨星辰2”的评论

感觉你有2个地方实现有点问题:
1:get方法里面那个锁直接new一个新的,这块的同步不就失去作用了吗?
2:redis那里清除缓存的时候使用keys 这个命令,生产环境在key非常多的情况下肯定有问题,可以考虑从一级缓存中取出所有的key 然后再遍历删除

我自己也实现了一套二级缓存,不过都是包装springcache中已经实现好的cache,选取2个,一个做本地缓存,一个做远程缓存

引用来自“J猿”的评论

1.get方法里面的锁的方式已经更新了,请参考码云上的源码,文章里没做更新
2.redis清除缓存使用keys命令一般不会有问题,因为缓存key是有统一的前缀的,除非与应用中其他缓存的前缀冲突,则会有问题。另外一级缓存也有有效期,所以从一级缓存获取key可能不完整

你实现的两级缓存如果有源码的话可以共享出来学习一下哈。spring boot中spring cache已经实现的redis缓存是有一些问题的,最好不要使用

引用来自“丶璀璨星辰2”的评论

嗯 后面我考虑了不能从一级缓存里面取出key,因为失效时间和逐出算法都不一样。可以参考redis的实现,它是把cname的key 放入zset里面,清除时是从zset中取出
你的源码呢?发出来呗
2018/04/13 10:56
回复
举报
大哥启动报错啊。。。available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2018/04/13 10:55
回复
举报

引用来自“丶璀璨星辰2”的评论

感觉你有2个地方实现有点问题:
1:get方法里面那个锁直接new一个新的,这块的同步不就失去作用了吗?
2:redis那里清除缓存的时候使用keys 这个命令,生产环境在key非常多的情况下肯定有问题,可以考虑从一级缓存中取出所有的key 然后再遍历删除

我自己也实现了一套二级缓存,不过都是包装springcache中已经实现好的cache,选取2个,一个做本地缓存,一个做远程缓存

引用来自“J猿”的评论

1.get方法里面的锁的方式已经更新了,请参考码云上的源码,文章里没做更新
2.redis清除缓存使用keys命令一般不会有问题,因为缓存key是有统一的前缀的,除非与应用中其他缓存的前缀冲突,则会有问题。另外一级缓存也有有效期,所以从一级缓存获取key可能不完整

你实现的两级缓存如果有源码的话可以共享出来学习一下哈。spring boot中spring cache已经实现的redis缓存是有一些问题的,最好不要使用
嗯 后面我考虑了不能从一级缓存里面取出key,因为失效时间和逐出算法都不一样。可以参考redis的实现,它是把cname的key 放入zset里面,清除时是从zset中取出
2018/02/27 11:33
回复
举报
Mr---D博主

引用来自“丶璀璨星辰2”的评论

引用来自“J猿”的评论

引用来自“丶璀璨星辰2”的评论

感觉你有2个地方实现有点问题:
1:get方法里面那个锁直接new一个新的,这块的同步不就失去作用了吗?
2:redis那里清除缓存的时候使用keys 这个命令,生产环境在key非常多的情况下肯定有问题,可以考虑从一级缓存中取出所有的key 然后再遍历删除

我自己也实现了一套二级缓存,不过都是包装springcache中已经实现好的cache,选取2个,一个做本地缓存,一个做远程缓存
1.get方法里面的锁的方式已经更新了,请参考码云上的源码,文章里没做更新
2.redis清除缓存使用keys命令一般不会有问题,因为缓存key是有统一的前缀的,除非与应用中其他缓存的前缀冲突,则会有问题。另外一级缓存也有有效期,所以从一级缓存获取key可能不完整

你实现的两级缓存如果有源码的话可以共享出来学习一下哈。spring boot中spring cache已经实现的redis缓存是有一些问题的,最好不要使用

我说的那个keys问题不是冲突,是性能问题
这个性能问题确实还没考虑过,因为这里清理缓存对性能要求并不会太高,如果确实要考虑性能问题,也建议是用其他方式去做,而不是从一级缓存中去获取key
2018/02/27 10:56
回复
举报

引用来自“J猿”的评论

引用来自“丶璀璨星辰2”的评论

感觉你有2个地方实现有点问题:
1:get方法里面那个锁直接new一个新的,这块的同步不就失去作用了吗?
2:redis那里清除缓存的时候使用keys 这个命令,生产环境在key非常多的情况下肯定有问题,可以考虑从一级缓存中取出所有的key 然后再遍历删除

我自己也实现了一套二级缓存,不过都是包装springcache中已经实现好的cache,选取2个,一个做本地缓存,一个做远程缓存
1.get方法里面的锁的方式已经更新了,请参考码云上的源码,文章里没做更新
2.redis清除缓存使用keys命令一般不会有问题,因为缓存key是有统一的前缀的,除非与应用中其他缓存的前缀冲突,则会有问题。另外一级缓存也有有效期,所以从一级缓存获取key可能不完整

你实现的两级缓存如果有源码的话可以共享出来学习一下哈。spring boot中spring cache已经实现的redis缓存是有一些问题的,最好不要使用

我说的那个keys问题不是冲突,是性能问题
2018/02/26 20:25
回复
举报
更多评论
打赏
32 评论
93 收藏
22
分享
返回顶部
顶部