springboot中使用自定义两级缓存的方法

网友投稿 256 2023-02-01


springboot中使用自定义两级缓存的方法

工作中用到了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)http://")

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

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

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 {

@Override

public void onApplicationEvent(ContextRefreshedEvent event) {

// 判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次)

if(event.getApplicationContext().getParent()==null){

//得到所有被@EnableCaching注解修饰的类

Map 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 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 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 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 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

*/

public static 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 deserialize(String src, Class 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"

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

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 {

@Override

public void onApplicationEvent(ContextRefreshedEvent event) {

// 判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次)

if(event.getApplicationContext().getParent()==null){

//得到所有被@EnableCaching注解修饰的类

Map 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 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 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 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 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

*/

public static 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 deserialize(String src, Class 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"


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:karma+webpack搭建vue单元测试环境的方法示例
下一篇:高可用共享文件系统(内部文件共享系统)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~