工作中用到了 springboot 的缓存, 使用起来挺方便的, 直接引入 redis 或者 ehcache 这些缓存依赖包和相关缓存的 starter 依赖包, 然后在启动类中加入 @EnableCaching 注解, 然后在需要的地方就可以使用 @Cacheable 和 @CacheEvict 使用和删除缓存了. 这个使用很简单, 相信用过 springboot 缓存的都会玩, 这里就不再多说了. 美中不足的是, springboot 使用了插件式的集成方式, 虽然用起来很方便, 但是当你集成 ehcache 的时候就是用 ehcache, 集成 redis 的时候就是用 redis. 如果想两者一起用, ehcache 作为本地一级缓存, redis 作为集成式的二级缓存, 使用默认的方式据我所知是没法实现的(如果有高人可以实现, 麻烦指点下我). 毕竟很多服务需要多点部署, 如果单独选择 ehcache 可以很好地实现本地缓存, 但是如果在多机之间共享缓存又需要比较费时的折腾, 如果选用集中式的 redis 缓存, 因为每次取数据都要走网络, 总感觉性能不会太好. 本话题主要就是讨论如何在 springboot 的基础上, 无缝集成 ehcache 和 redis 作为一二级缓存, 并且实现缓存同步.
为了不要侵入 springboot 原本使用缓存的方式, 这里自己定义了两个缓存相关的注解, 如下
- @Target({ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Cacheable {
- String value() default "";
- String key() default "";
- // 泛型的 Class 类型
- Class<?> type() default Exception.class;
- }
- @Target({ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- public @interface CacheEvict {
- String value() default "";
- String key() default "";
- }
如上两个注解和 spring 中缓存的注解基本一致, 只是去掉了一些不常用的属性. 说到这里, 不知道有没有朋友注意过, 当你在 springboot 中单独使用 redis 缓存的时候, Cacheable 和 CacheEvict 注解的 value 属性, 实际上在 redis 中变成了一个 zset 类型的值的 key, 而且这个 zset 里面还是空的, 比如 @Cacheable(value="cache1",key="key1"), 正常情况下 redis 中应该是出现 cache1 -> map(key1,value1)这种形式, 其中 cache1 作为缓存名称, map 作为缓存的值, key 作为 map 里的键, 可以有效的隔离不同的缓存名称下的缓存. 但是实际上 redis 里确是 cache1 -> 空 (zset) 和 key1 -> value1, 两个独立的键值对, 试验得知不同的缓存名称下的缓存完全是共用的, 如果有感兴趣的朋友可以去试验下, 也就是说这个 value 属性实际上是个摆设, 键的唯一性只由 key 属性保证. 我只能认为这是 spring 的缓存实现的 bug, 或者是特意这么设计的,(如果有知道啥原因的欢迎指点).
回到正题, 有了注解还需要有个注解处理类, 这里我使用 aop 的切面来进行拦截处理, 原生的实现其实也大同小异. 切面处理类如下:
- import com.xuanwu.apaas.core.multicache.annotation.CacheEvict;
- import com.xuanwu.apaas.core.multicache.annotation.Cacheable;
- import com.xuanwu.apaas.core.utils.JsonUtil;
- import org.apache.commons.lang3.StringUtils;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.json.JSONArray;
- import org.json.JSONObject;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
- import org.springframework.expression.ExpressionParser;
- import org.springframework.expression.spel.standard.SpelExpressionParser;
- import org.springframework.expression.spel.support.StandardEvaluationContext;
- import org.springframework.stereotype.Component;
- import java.lang.reflect.Method;
- /**
- * 多级缓存切面
- * @author rongdi
- */
- @Aspect
- @Component
- public class MultiCacheAspect {
- private static final Logger logger = LoggerFactory.getLogger(MultiCacheAspect.class);
- @Autowired
- private CacheFactory cacheFactory;
- // 这里通过一个容器初始化监听器, 根据外部配置的 @EnableCaching 注解控制缓存开关
- private boolean cacheEnable;
- @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)")
- public void cacheableAspect() {
- }
- @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)")
- public void cacheEvict() {
- }
- @Around("cacheableAspect()")
- public Object cache(ProceedingJoinPoint joinPoint) {
- // 得到被切面修饰的方法的参数列表
- Object[] args = joinPoint.getArgs();
- // result 是方法的最终返回结果
- Object result = null;
- // 如果没有开启缓存, 直接调用处理方法返回
- if(!cacheEnable){
- try {
- result = joinPoint.proceed(args);
- } catch (Throwable e) {
- logger.error("",e);
- }
- return result;
- }
- // 得到被代理方法的返回值类型
- Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
- // 得到被代理的方法
- Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
- // 得到被代理的方法上的注解
- Cacheable ca = method.getAnnotation(Cacheable.class);
- // 获得经过 el 解析后的 key 值
- String key = parseKey(ca.key(),method,args);
- Class<?> elementClass = ca.type();
- // 从注解中获取缓存名称
- String name = ca.value();
- try {
- // 先从 ehcache 中取数据
- String cacheValue = cacheFactory.ehGet(name,key);
- if(StringUtils.isEmpty(cacheValue)) {
- // 如果 ehcache 中没数据, 从 redis 中取数据
- cacheValue = cacheFactory.redisGet(name,key);
- if(StringUtils.isEmpty(cacheValue)) {
- // 如果 redis 中没有数据
- // 调用业务方法得到结果
- result = joinPoint.proceed(args);
- // 将结果序列化后放入 redis
- cacheFactory.redisPut(name,key,serialize(result));
- } else {
- // 如果 redis 中可以取到数据
- // 将缓存中获取到的数据反序列化后返回
- if(elementClass == Exception.class) {
- result = deserialize(cacheValue, returnType);
- } else {
- result = deserialize(cacheValue, returnType,elementClass);
- }
- }
- // 将结果序列化后放入 ehcache
- cacheFactory.ehPut(name,key,serialize(result));
- } else {
- // 将缓存中获取到的数据反序列化后返回
- if(elementClass == Exception.class) {
- result = deserialize(cacheValue, returnType);
- } else {
- result = deserialize(cacheValue, returnType,elementClass);
- }
- }
- } catch (Throwable throwable) {
- logger.error("",throwable);
- }
- return result;
- }
- /**
- * 在方法调用前清除缓存, 然后调用业务方法
- * @param joinPoint
- * @return
- * @throws Throwable
- *
- */
- @Around("cacheEvict()")
- public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable {
- // 得到被代理的方法
- Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
- // 得到被切面修饰的方法的参数列表
- Object[] args = joinPoint.getArgs();
- // 得到被代理的方法上的注解
- CacheEvict ce = method.getAnnotation(CacheEvict.class);
- // 获得经过 el 解析后的 key 值
- String key = parseKey(ce.key(),method,args);
- // 从注解中获取缓存名称
- String name = ce.value();
- // 清除对应缓存
- cacheFactory.cacheDel(name,key);
- return joinPoint.proceed(args);
- }
- /**
- * 获取缓存的 key
- * key 定义在注解上, 支持 SPEL 表达式
- * @return
- */
- private String parseKey(String key,Method method,Object [] args){
- if(StringUtils.isEmpty(key)) return null;
- // 获取被拦截方法参数名列表(使用 Spring 支持类库)
- LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
- String[] paraNameArr = u.getParameterNames(method);
- // 使用 SPEL 进行 key 的解析
- ExpressionParser parser = new SpelExpressionParser();
- //SPEL 上下文
- StandardEvaluationContext context = new StandardEvaluationContext();
- // 把方法参数放入 SPEL 上下文中
- for(int i=0;i<paraNameArr.length;i++){
- context.setVariable(paraNameArr[i], args[i]);
- }
- return parser.parseExpression(key).getValue(context,String.class);
- }
- // 序列化
- private String serialize(Object obj) {
- String result = null;
- try {
- result = JsonUtil.serialize(obj);
- } catch(Exception e) {
- result = obj.toString();
- }
- return result;
- }
- // 反序列化
- private Object deserialize(String str,Class clazz) {
- Object result = null;
- try {
- if(clazz == JSONObject.class) {
- result = new JSONObject(str);
- } else if(clazz == JSONArray.class) {
- result = new JSONArray(str);
- } else {
- result = JsonUtil.deserialize(str,clazz);
- }
- } catch(Exception e) {
- }
- return result;
- }
- // 反序列化, 支持 List<xxx>
- private Object deserialize(String str,Class clazz,Class elementClass) {
- Object result = null;
- try {
- if(clazz == JSONObject.class) {
- result = new JSONObject(str);
- } else if(clazz == JSONArray.class) {
- result = new JSONArray(str);
- } else {
- result = JsonUtil.deserialize(str,clazz,elementClass);
- }
- } catch(Exception e) {
- }
- return result;
- }
- public void setCacheEnable(boolean cacheEnable) {
- this.cacheEnable = cacheEnable;
- }
- }
上面这个界面使用了一个 cacheEnable 变量控制是否使用缓存, 为了实现无缝的接入 springboot, 必然需要受到原生 @EnableCaching 注解的控制, 这里我使用一个 spring 容器加载完成的监听器, 然后在监听器里找到是否有被 @EnableCaching 注解修饰的类, 如果有就从 spring 容器拿到 MultiCacheAspect 对象, 然后将 cacheEnable 设置成 true. 这样就可以实现无缝接入 springboot, 不知道朋友们还有没有更加优雅的方法呢? 欢迎交流! 监听器类如下
- import com.xuanwu.apaas.core.multicache.CacheFactory;
- import com.xuanwu.apaas.core.multicache.MultiCacheAspect;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.context.ApplicationListener;
- import org.springframework.context.event.ContextRefreshedEvent;
- import org.springframework.stereotype.Component;
- import java.util.Map;
- /**
- * 用于 spring 加载完成后, 找到项目中是否有开启缓存的注解 @EnableCaching
- * @author rongdi
- */
- @Component
- public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
- @Override
- public void onApplicationEvent(ContextRefreshedEvent event) {
- // 判断根容器为 Spring 容器, 防止出现调用两次的情况(mvc 加载也会触发一次)
- if(event.getApplicationContext().getParent()==null){
- // 得到所有被 @EnableCaching 注解修饰的类
- Map<String,Object> beans = event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class);
- if(beans != null && !beans.isEmpty()) {
- MultiCacheAspect multiCache = (MultiCacheAspect)event.getApplicationContext().getBean("multiCacheAspect");
- multiCache.setCacheEnable(true);
- }
- }
- }
- }
实现了无缝接入, 还需要考虑多点部署的时候, 多点的 ehcache 怎么和 redis 缓存保持一致的问题. 在正常应用中, 一般 redis 适合长时间的集中式缓存, ehcache 适合短时间的本地缓存, 假设现在有 A,B 和 C 服务器, A 和 B 部署了业务服务, C 部署了 redis 服务. 当请求进来, 前端入口不管是用 LVS 或者 nginx 等负载软件, 请求都会转发到某一个具体服务器, 假设转发到了 A 服务器, 修改了某个内容, 而这个内容在 redis 和 ehcache 中都有, 这时候, A 服务器的 ehcache 缓存, 和 C 服务器的 redis 不管控制缓存失效也好, 删除也好, 都比较容易, 但是这时候 B 服务器的 ehcache 怎么控制失效或者删除呢? 一般比较常用的方式就是使用发布订阅模式, 当需要删除缓存的时候在一个固定的通道发布一个消息, 然后每个业务服务器订阅这个通道, 收到消息后删除或者过期本地的 ehcache 缓存(最好是使用过期, 但是 redis 目前只支持对 key 的过期操作, 没办法操作 key 下的 map 里的成员的过期, 如果非要强求用过期, 可以自己加时间戳自己实现, 不过用删除出问题的几率也很小, 毕竟加缓存的都是读多写少的应用, 这里为了方便都是直接删除缓存). 总结起来流程就是更新某条数据, 先删除 redis 中对应的缓存, 然后发布一个缓存失效的消息在 redis 的某个通道中, 本地的业务服务去订阅这个通道的消息, 当业务服务收到这个消息后去删除本地对应的 ehcache 缓存, redis 的各种配置如下
- import com.fasterxml.jackson.annotation.JsonAutoDetect;
- import com.fasterxml.jackson.annotation.PropertyAccessor;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber;
- import org.springframework.cache.CacheManager;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.cache.RedisCacheManager;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.listener.PatternTopic;
- import org.springframework.data.redis.listener.RedisMessageListenerContainer;
- import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
- import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
- @Configuration
- public class RedisConfig {
- @Bean
- public CacheManager cacheManager(RedisTemplate redisTemplate) {
- RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
- // 设置缓存过期时间(秒)
- rcm.setDefaultExpiration(600);
- return rcm;
- }
- @Bean
- public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
- StringRedisTemplate template = new StringRedisTemplate(factory);
- Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
- ObjectMapper om = new ObjectMapper();
- om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
- jackson2JsonRedisSerializer.setObjectMapper(om);
- template.setValueSerializer(jackson2JsonRedisSerializer);
- template.afterPropertiesSet();
- return template;
- }
- /**
- * redis 消息监听器容器
- * 可以添加多个监听不同话题的 redis 监听器, 只需要把消息监听器和相应的消息订阅处理器绑定, 该消息监听器
- * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
- * @param connectionFactory
- * @param listenerAdapter
- * @return
- */
- @Bean
- public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
- MessageListenerAdapter listenerAdapter) {
- RedisMessageListenerContainer container = new RedisMessageListenerContainer();
- container.setConnectionFactory(connectionFactory);
- // 订阅了一个叫 redis.uncache 的通道
- container.addMessageListener(listenerAdapter, new PatternTopic("redis.uncache"));
- // 这个 container 可以添加多个 messageListener
- return container;
- }
- /**
- * 消息监听器适配器, 绑定消息处理器, 利用反射技术调用消息处理器的业务方法
- * @param receiver
- * @return
- */
- @Bean
- MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) {
- // 这个地方 是给 messageListenerAdapter 传入一个消息接受的处理器, 利用反射的方法调用 "handle"
- return new MessageListenerAdapter(receiver, "handle");
- }
- }
消息发布类如下:
- import com.xuanwu.apaas.core.multicache.CacheFactory;
- import org.apache.commons.lang3.StringUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- @Component
- public class MessageSubscriber {
- private static final Logger logger = LoggerFactory.getLogger(MessageSubscriber.class);
- @Autowired
- private CacheFactory cacheFactory;
- /**
- * 接收到 redis 订阅的消息后, 将 ehcache 的缓存失效
- * @param message 格式为 name_key
- */
- public void handle(String message){
- logger.debug("redis.ehcache:"+message);
- if(StringUtils.isEmpty(message)) {
- return;
- }
- String[] strs = message.split("#");
- String name = strs[0];
- String key = null;
- if(strs.length == 2) {
- key = strs[1];
- }
- cacheFactory.ehDel(name,key);
- }
- }
具体操作缓存的类如下:
- import com.xuanwu.apaas.core.multicache.publisher.MessagePublisher;
- import net.sf.ehcache.Cache;
- import net.sf.ehcache.CacheManager;
- import net.sf.ehcache.Element;
- import org.apache.commons.lang3.StringUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.RedisConnectionFailureException;
- import org.springframework.data.redis.core.HashOperations;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.stereotype.Component;
- import java.io.InputStream;
- /**
- * 多级缓存切面
- * @author rongdi
- */
- @Component public class CacheFactory {
- private static final Logger logger = LoggerFactory.getLogger(CacheFactory.class);@Autowired private RedisTemplate redisTemplate;@Autowired private MessagePublisher messagePublisher;
- private CacheManager cacheManager;
- public CacheFactory() {
- InputStream is = this.getClass().getResourceAsStream("/ehcache.xml");
- if (is != null) {
- cacheManager = CacheManager.create(is);
- }
- }
- public void cacheDel(String name, String key) {
- // 删除 redis 对应的缓存
- redisDel(name, key);
- // 删除本地的 ehcache 缓存, 可以不需要, 订阅器那里会删除
- // ehDel(name,key);
- if (cacheManager != null) {
- // 发布一个消息, 告诉订阅的服务该缓存失效
- messagePublisher.publish(name, key);
- }
- }
- public String ehGet(String name, String key) {
- if (cacheManager == null) return null;
- Cache cache = cacheManager.getCache(name);
- if (cache == null) return null;
- cache.acquireReadLockOnKey(key);
- try {
- Element ele = cache.get(key);
- if (ele == null) return null;
- return (String) ele.getObjectValue();
- } finally {
- cache.releaseReadLockOnKey(key);
- }
- }
- public String redisGet(String name, String key) {
- HashOperations <String,
- String,
- String> oper = redisTemplate.opsForHash();
- try {
- return oper.get(name, key);
- } catch(RedisConnectionFailureException e) {
- // 连接失败, 不抛错, 直接不用 redis 缓存了
- logger.error("connect redis error", e);
- return null;
- }
- }
- public void ehPut(String name, String key, String value) {
- if (cacheManager == null) return;
- if (!cacheManager.cacheExists(name)) {
- cacheManager.addCache(name);
- }
- Cache cache = cacheManager.getCache(name);
- // 获得 key 上的写锁, 不同 key 互相不影响, 类似于 synchronized(key.intern()){}
- cache.acquireWriteLockOnKey(key);
- try {
- cache.put(new Element(key, value));
- } finally {
- // 释放写锁
- cache.releaseWriteLockOnKey(key);
- }
- }
- public void redisPut(String name, String key, String value) {
- HashOperations <String,
- String,
- String> oper = redisTemplate.opsForHash();
- try {
- oper.put(name, key, value);
- } catch(RedisConnectionFailureException e) {
- // 连接失败, 不抛错, 直接不用 redis 缓存了
- logger.error("connect redis error", e);
- }
- }
- public void ehDel(String name, String key) {
- if (cacheManager == null) return;
- if (cacheManager.cacheExists(name)) {
- // 如果 key 为空, 直接根据缓存名删除
- if (StringUtils.isEmpty(key)) {
- cacheManager.removeCache(name);
- } else {
- Cache cache = cacheManager.getCache(name);
- cache.remove(key);
- }
- }
- }
- public void redisDel(String name, String key) {
- HashOperations <String,
- String,
- String> oper = redisTemplate.opsForHash();
- try {
- // 如果 key 为空, 直接根据缓存名删除
- if (StringUtils.isEmpty(key)) {
- redisTemplate.delete(name);
- } else {
- oper.delete(name, key);
- }
- } catch(RedisConnectionFailureException e) {
- // 连接失败, 不抛错, 直接不用 redis 缓存了
- logger.error("connect redis error", e);
- }
- }
- }
工具类如下
- import com.fasterxml.jackson.core.type.TypeReference;
- import com.fasterxml.jackson.databind.DeserializationFeature;
- import com.fasterxml.jackson.databind.JavaType;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.apache.commons.lang3.StringUtils;
- import org.json.JSONArray;
- import org.json.JSONObject;
- import java.util.*;
- public class JsonUtil {
- private static ObjectMapper mapper;
- static {
- mapper = new ObjectMapper();
- mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
- false);
- }
- /**
- * 将对象序列化成 json
- *
- * @param obj 待序列化的对象
- * @return
- * @throws Exception
- */
- public static String serialize(Object obj) throws Exception {
- if (obj == null) {
- throw new IllegalArgumentException("obj should not be null");
- }
- return mapper.writeValueAsString(obj);
- }
- /**
- 带泛型的反序列化, 比如一个 JSONArray 反序列化成 List<User>
- */
- public static <T> T deserialize(String jsonStr, Class<?> collectionClass,
- Class<?>... elementClasses) throws Exception {
- JavaType javaType = mapper.getTypeFactory().constructParametrizedType(
- collectionClass, collectionClass, elementClasses);
- return mapper.readValue(jsonStr, javaType);
- }
- /**
- * 将 json 字符串反序列化成对象
- * @param src 待反序列化的 json 字符串
- * @param t 反序列化成为的对象的 class 类型
- * @return
- * @throws Exception
- */
- public static <T> T deserialize(String src, Class<T> t) throws Exception {
- if (src == null) {
- throw new IllegalArgumentException("src should not be null");
- }
- if("{}".equals(src.trim())) {
- return null;
- }
- return mapper.readValue(src, t);
- }
- }
具体使用缓存, 和之前一样只需要关注 @Cacheable 和 @CacheEvict 注解, 同样也支持 spring 的 el 表达式. 而且这里的 value 属性表示的缓存名称也没有上面说的那个问题, 完全可以用 value 隔离不同的缓存, 例子如下
- @Cacheable(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
- @CacheEvict(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
附上主要的依赖包
- "org.springframework.boot:spring-boot-starter-redis:1.4.2.RELEASE",
- 'net.sf.ehcache:ehcache:2.10.4',
- "org.json:json:20160810"
来源: https://www.cnblogs.com/rongdi/p/9057208.html