工作中用到了springboot的缓存,使用起来挺方便的,直接引入redis或者ehcache这些缓存依赖包和相关缓存的starter依赖包,然后在启动类中加入@EnableCaching注解,然后在需要的地方就可以使用@Cacheable和@CacheEvict使用和删除缓存了。这个使用很简单,相信用过springboot缓存的都会玩,这里就不再多说了。美中不足的是,springboot使用了插件式的集成方式,虽然用起来很方便,但是当你集成ehcache的时候就是用ehcache,集成redis的时候就是用redis。如果想两者一起用,ehcache作为本地一级缓存,redis作为集成式的二级缓存,使用默认的方式据我所知是没法实现的(如果有高人可以实现,麻烦指点下我)。毕竟很多服务需要多点部署,如果单独选择ehcache可以很好地实现本地缓存,但是如果在多机之间共享缓存又需要比较费时的折腾,如果选用集中式的redis缓存,因为每次取数据都要走网络,总感觉性能不会太好。本话题主要就是讨论如何在springboot的基础上,无缝集成ehcache和redis作为一二级缓存,并且实现缓存同步。

    为了不要侵入springboot原本使用缓存的方式,这里自己定义了两个缓存相关的注解,如下:

    1. @Target({ElementType.METHOD})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. public @interface Cacheable {
    4. String value() default "";
    5. String key() default "";
    6. //泛型的Class类型
    7. Class<?> type() default Exception.class;
    8. }
    9. @Target({ElementType.METHOD})
    10. @Retention(RetentionPolicy.RUNTIME)
    11. public @interface CacheEvict {
    12. String value() default "";
    13. String key() default "";
    14. }

    如上两个注解和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的切面来进行拦截处理,原生的实现其实也大同小异。切面处理类如下:

    1. import com.xuanwu.apaas.core.multicache.annotation.CacheEvict;
    2. import com.xuanwu.apaas.core.multicache.annotation.Cacheable;
    3. import com.xuanwu.apaas.core.utils.JsonUtil;
    4. import org.apache.commons.lang3.StringUtils;
    5. import org.aspectj.lang.ProceedingJoinPoint;
    6. import org.aspectj.lang.annotation.Around;
    7. import org.aspectj.lang.annotation.Aspect;
    8. import org.aspectj.lang.annotation.Pointcut;
    9. import org.aspectj.lang.reflect.MethodSignature;
    10. import org.json.JSONArray;
    11. import org.json.JSONObject;
    12. import org.slf4j.Logger;
    13. import org.slf4j.LoggerFactory;
    14. import org.springframework.beans.factory.annotation.Autowired;
    15. import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
    16. import org.springframework.expression.ExpressionParser;
    17. import org.springframework.expression.spel.standard.SpelExpressionParser;
    18. import org.springframework.expression.spel.support.StandardEvaluationContext;
    19. import org.springframework.stereotype.Component;
    20. import java.lang.reflect.Method;
    21. /**
    22. * 多级缓存切面
    23. * @author rongdi
    24. */
    25. @Aspect
    26. @Component
    27. public class MultiCacheAspect {
    28. private static final Logger logger = LoggerFactory.getLogger(MultiCacheAspect.class);
    29. @Autowired
    30. private CacheFactory cacheFactory;
    31. //这里通过一个容器初始化监听器,根据外部配置的@EnableCaching注解控制缓存开关
    32. private boolean cacheEnable;
    33. @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)")
    34. public void cacheableAspect() {
    35. }
    36. @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)")
    37. public void cacheEvict() {
    38. }
    39. @Around("cacheableAspect()")
    40. public Object cache(ProceedingJoinPoint joinPoint) {
    41. //得到被切面修饰的方法的参数列表
    42. Object[] args = joinPoint.getArgs();
    43. // result是方法的最终返回结果
    44. Object result = null;
    45. //如果没有开启缓存,直接调用处理方法返回
    46. if(!cacheEnable){
    47. try {
    48. result = joinPoint.proceed(args);
    49. } catch (Throwable e) {
    50. logger.error("",e);
    51. }
    52. return result;
    53. }
    54. // 得到被代理方法的返回值类型
    55. Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
    56. // 得到被代理的方法
    57. Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    58. // 得到被代理的方法上的注解
    59. Cacheable ca = method.getAnnotation(Cacheable.class);
    60. //获得经过el解析后的key值
    61. String key = parseKey(ca.key(),method,args);
    62. Class<?> elementClass = ca.type();
    63. //从注解中获取缓存名称
    64. String name = ca.value();
    65. try {
    66. //先从ehcache中取数据
    67. String cacheValue = cacheFactory.ehGet(name,key);
    68. if(StringUtils.isEmpty(cacheValue)) {
    69. //如果ehcache中没数据,从redis中取数据
    70. cacheValue = cacheFactory.redisGet(name,key);
    71. if(StringUtils.isEmpty(cacheValue)) {
    72. //如果redis中没有数据
    73. // 调用业务方法得到结果
    74. result = joinPoint.proceed(args);
    75. //将结果序列化后放入redis
    76. cacheFactory.redisPut(name,key,serialize(result));
    77. } else {
    78. //如果redis中可以取到数据
    79. //将缓存中获取到的数据反序列化后返回
    80. if(elementClass == Exception.class) {
    81. result = deserialize(cacheValue, returnType);
    82. } else {
    83. result = deserialize(cacheValue, returnType,elementClass);
    84. }
    85. }
    86. //将结果序列化后放入ehcache
    87. cacheFactory.ehPut(name,key,serialize(result));
    88. } else {
    89. //将缓存中获取到的数据反序列化后返回
    90. if(elementClass == Exception.class) {
    91. result = deserialize(cacheValue, returnType);
    92. } else {
    93. result = deserialize(cacheValue, returnType,elementClass);
    94. }
    95. }
    96. } catch (Throwable throwable) {
    97. logger.error("",throwable);
    98. }
    99. return result;
    100. }
    101. /**
    102. * 在方法调用前清除缓存,然后调用业务方法
    103. * @param joinPoint
    104. * @return
    105. * @throws Throwable
    106. *
    107. */
    108. @Around("cacheEvict()")
    109. public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable {
    110. // 得到被代理的方法
    111. Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    112. //得到被切面修饰的方法的参数列表
    113. Object[] args = joinPoint.getArgs();
    114. // 得到被代理的方法上的注解
    115. CacheEvict ce = method.getAnnotation(CacheEvict.class);
    116. //获得经过el解析后的key值
    117. String key = parseKey(ce.key(),method,args);
    118. //从注解中获取缓存名称
    119. String name = ce.value();
    120. // 清除对应缓存
    121. cacheFactory.cacheDel(name,key);
    122. return joinPoint.proceed(args);
    123. }
    124. /**
    125. * 获取缓存的key
    126. * key 定义在注解上,支持SPEL表达式
    127. * @return
    128. */
    129. private String parseKey(String key,Method method,Object [] args){
    130. if(StringUtils.isEmpty(key)) return null;
    131. //获取被拦截方法参数名列表(使用Spring支持类库)
    132. LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
    133. String[] paraNameArr = u.getParameterNames(method);
    134. //使用SPEL进行key的解析
    135. ExpressionParser parser = new SpelExpressionParser();
    136. //SPEL上下文
    137. StandardEvaluationContext context = new StandardEvaluationContext();
    138. //把方法参数放入SPEL上下文中
    139. for(int i=0;i<paraNameArr.length;i++){
    140. context.setVariable(paraNameArr[i], args[i]);
    141. }
    142. return parser.parseExpression(key).getValue(context,String.class);
    143. }
    144. //序列化
    145. private String serialize(Object obj) {
    146. String result = null;
    147. try {
    148. result = JsonUtil.serialize(obj);
    149. } catch(Exception e) {
    150. result = obj.toString();
    151. }
    152. return result;
    153. }
    154. //反序列化
    155. private Object deserialize(String str,Class clazz) {
    156. Object result = null;
    157. try {
    158. if(clazz == JSONObject.class) {
    159. result = new JSONObject(str);
    160. } else if(clazz == JSONArray.class) {
    161. result = new JSONArray(str);
    162. } else {
    163. result = JsonUtil.deserialize(str,clazz);
    164. }
    165. } catch(Exception e) {
    166. }
    167. return result;
    168. }
    169. //反序列化,支持List<xxx>
    170. private Object deserialize(String str,Class clazz,Class elementClass) {
    171. Object result = null;
    172. try {
    173. if(clazz == JSONObject.class) {
    174. result = new JSONObject(str);
    175. } else if(clazz == JSONArray.class) {
    176. result = new JSONArray(str);
    177. } else {
    178. result = JsonUtil.deserialize(str,clazz,elementClass);
    179. }
    180. } catch(Exception e) {
    181. }
    182. return result;
    183. }
    184. public void setCacheEnable(boolean cacheEnable) {
    185. this.cacheEnable = cacheEnable;
    186. }
    187. }

    上面这个界面使用了一个cacheEnable变量控制是否使用缓存,为了实现无缝的接入springboot,必然需要受到原生@EnableCaching注解的控制,这里我使用一个spring容器加载完成的监听器,然后在监听器里找到是否有被@EnableCaching注解修饰的类,如果有就从spring容器拿到MultiCacheAspect对象,然后将cacheEnable设置成true。这样就可以实现无缝接入springboot,不知道朋友们还有没有更加优雅的方法呢?欢迎交流!监听器类如下:

    1. import com.xuanwu.apaas.core.multicache.CacheFactory;
    2. import com.xuanwu.apaas.core.multicache.MultiCacheAspect;
    3. import org.springframework.cache.annotation.EnableCaching;
    4. import org.springframework.context.ApplicationListener;
    5. import org.springframework.context.event.ContextRefreshedEvent;
    6. import org.springframework.stereotype.Component;
    7. import java.util.Map;
    8. /**
    9. * 用于spring加载完成后,找到项目中是否有开启缓存的注解@EnableCaching
    10. * @author rongdi
    11. */
    12. @Component
    13. public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
    14. @Override
    15. public void onApplicationEvent(ContextRefreshedEvent event) {
    16. // 判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次)
    17. if(event.getApplicationContext().getParent()==null){
    18. //得到所有被@EnableCaching注解修饰的类
    19. Map<String,Object> beans = event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class);
    20. if(beans != null && !beans.isEmpty()) {
    21. MultiCacheAspect multiCache = (MultiCacheAspect)event.getApplicationContext().getBean("multiCacheAspect");
    22. multiCache.setCacheEnable(true);
    23. }
    24. }
    25. }
    26. }

    实现了无缝接入,还需要考虑多点部署的时候,多点的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的各种配置如下:

    1. import com.fasterxml.jackson.annotation.JsonAutoDetect;
    2. import com.fasterxml.jackson.annotation.PropertyAccessor;
    3. import com.fasterxml.jackson.databind.ObjectMapper;
    4. import com.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber;
    5. import org.springframework.cache.CacheManager;
    6. import org.springframework.context.annotation.Bean;
    7. import org.springframework.context.annotation.Configuration;
    8. import org.springframework.data.redis.cache.RedisCacheManager;
    9. import org.springframework.data.redis.connection.RedisConnectionFactory;
    10. import org.springframework.data.redis.core.RedisTemplate;
    11. import org.springframework.data.redis.core.StringRedisTemplate;
    12. import org.springframework.data.redis.listener.PatternTopic;
    13. import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    14. import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
    15. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    16. @Configuration
    17. public class RedisConfig {
    18. @Bean
    19. public CacheManager cacheManager(RedisTemplate redisTemplate) {
    20. RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
    21. //设置缓存过期时间(秒)
    22. rcm.setDefaultExpiration(600);
    23. return rcm;
    24. }
    25. @Bean
    26. public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
    27. StringRedisTemplate template = new StringRedisTemplate(factory);
    28. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    29. ObjectMapper om = new ObjectMapper();
    30. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    31. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    32. jackson2JsonRedisSerializer.setObjectMapper(om);
    33. template.setValueSerializer(jackson2JsonRedisSerializer);
    34. template.afterPropertiesSet();
    35. return template;
    36. }
    37. /**
    38. * redis消息监听器容器
    39. * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
    40. * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
    41. * @param connectionFactory
    42. * @param listenerAdapter
    43. * @return
    44. */
    45. @Bean
    46. public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
    47. MessageListenerAdapter listenerAdapter) {
    48. RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    49. container.setConnectionFactory(connectionFactory);
    50. //订阅了一个叫redis.uncache的通道
    51. container.addMessageListener(listenerAdapter, new PatternTopic("redis.uncache"));
    52. //这个container 可以添加多个 messageListener
    53. return container;
    54. }
    55. /**
    56. * 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
    57. * @param receiver
    58. * @return
    59. */
    60. @Bean
    61. MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) {
    62. //这个地方 是给messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“handle”
    63. return new MessageListenerAdapter(receiver, "handle");
    64. }
    65. }

    消息发布类如下:

    1. import com.xuanwu.apaas.core.multicache.CacheFactory;
    2. import org.apache.commons.lang3.StringUtils;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.stereotype.Component;
    7. @Component
    8. public class MessageSubscriber {
    9. private static final Logger logger = LoggerFactory.getLogger(MessageSubscriber.class);
    10. @Autowired
    11. private CacheFactory cacheFactory;
    12. /**
    13. * 接收到redis订阅的消息后,将ehcache的缓存失效
    14. * @param message 格式为name_key
    15. */
    16. public void handle(String message){
    17. logger.debug("redis.ehcache:"+message);
    18. if(StringUtils.isEmpty(message)) {
    19. return;
    20. }
    21. String[] strs = message.split("#");
    22. String name = strs[0];
    23. String key = null;
    24. if(strs.length == 2) {
    25. key = strs[1];
    26. }
    27. cacheFactory.ehDel(name,key);
    28. }
    29. }

    具体操作缓存的类如下:

    1. import com.xuanwu.apaas.core.multicache.publisher.MessagePublisher;
    2. import net.sf.ehcache.Cache;
    3. import net.sf.ehcache.CacheManager;
    4. import net.sf.ehcache.Element;
    5. import org.apache.commons.lang3.StringUtils;
    6. import org.slf4j.Logger;
    7. import org.slf4j.LoggerFactory;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.data.redis.RedisConnectionFailureException;
    10. import org.springframework.data.redis.core.HashOperations;
    11. import org.springframework.data.redis.core.RedisTemplate;
    12. import org.springframework.stereotype.Component;
    13. import java.io.InputStream;
    14. /**
    15. * 多级缓存切面
    16. * @author rongdi
    17. */
    18. @Component
    19. public class CacheFactory {
    20. private static final Logger logger = LoggerFactory.getLogger(CacheFactory.class);
    21. @Autowired
    22. private RedisTemplate redisTemplate;
    23. @Autowired
    24. private MessagePublisher messagePublisher;
    25. private CacheManager cacheManager;
    26. public CacheFactory() {
    27. InputStream is = this.getClass().getResourceAsStream("/ehcache.xml");
    28. if(is != null) {
    29. cacheManager = CacheManager.create(is);
    30. }
    31. }
    32. public void cacheDel(String name,String key) {
    33. //删除redis对应的缓存
    34. redisDel(name,key);
    35. //删除本地的ehcache缓存,可以不需要,订阅器那里会删除
    36. // ehDel(name,key);
    37. if(cacheManager != null) {
    38. //发布一个消息,告诉订阅的服务该缓存失效
    39. messagePublisher.publish(name, key);
    40. }
    41. }
    42. public String ehGet(String name,String key) {
    43. if(cacheManager == null) return null;
    44. Cache cache=cacheManager.getCache(name);
    45. if(cache == null) return null;
    46. cache.acquireReadLockOnKey(key);
    47. try {
    48. Element ele = cache.get(key);
    49. if(ele == null) return null;
    50. return (String)ele.getObjectValue();
    51. } finally {
    52. cache.releaseReadLockOnKey(key);
    53. }
    54. }
    55. public String redisGet(String name,String key) {
    56. HashOperations<String,String,String> oper = redisTemplate.opsForHash();
    57. try {
    58. return oper.get(name, key);
    59. } catch(RedisConnectionFailureException e) {
    60. //连接失败,不抛错,直接不用redis缓存了
    61. logger.error("connect redis error ",e);
    62. return null;
    63. }
    64. }
    65. public void ehPut(String name,String key,String value) {
    66. if(cacheManager == null) return;
    67. if(!cacheManager.cacheExists(name)) {
    68. cacheManager.addCache(name);
    69. }
    70. Cache cache=cacheManager.getCache(name);
    71. //获得key上的写锁,不同key互相不影响,类似于synchronized(key.intern()){}
    72. cache.acquireWriteLockOnKey(key);
    73. try {
    74. cache.put(new Element(key, value));
    75. } finally {
    76. //释放写锁
    77. cache.releaseWriteLockOnKey(key);
    78. }
    79. }
    80. public void redisPut(String name,String key,String value) {
    81. HashOperations<String,String,String> oper = redisTemplate.opsForHash();
    82. try {
    83. oper.put(name, key, value);
    84. } catch (RedisConnectionFailureException e) {
    85. //连接失败,不抛错,直接不用redis缓存了
    86. logger.error("connect redis error ",e);
    87. }
    88. }
    89. public void ehDel(String name,String key) {
    90. if(cacheManager == null) return;
    91. if(cacheManager.cacheExists(name)) {
    92. //如果key为空,直接根据缓存名删除
    93. if(StringUtils.isEmpty(key)) {
    94. cacheManager.removeCache(name);
    95. } else {
    96. Cache cache=cacheManager.getCache(name);
    97. cache.remove(key);
    98. }
    99. }
    100. }
    101. public void redisDel(String name,String key) {
    102. HashOperations<String,String,String> oper = redisTemplate.opsForHash();
    103. try {
    104. //如果key为空,直接根据缓存名删除
    105. if(StringUtils.isEmpty(key)) {
    106. redisTemplate.delete(name);
    107. } else {
    108. oper.delete(name,key);
    109. }
    110. } catch (RedisConnectionFailureException e) {
    111. //连接失败,不抛错,直接不用redis缓存了
    112. logger.error("connect redis error ",e);
    113. }
    114. }
    115. }

    工具类如下:

    1. import com.fasterxml.jackson.core.type.TypeReference;
    2. import com.fasterxml.jackson.databind.DeserializationFeature;
    3. import com.fasterxml.jackson.databind.JavaType;
    4. import com.fasterxml.jackson.databind.ObjectMapper;
    5. import org.apache.commons.lang3.StringUtils;
    6. import org.json.JSONArray;
    7. import org.json.JSONObject;
    8. import java.util.*;
    9. public class JsonUtil {
    10. private static ObjectMapper mapper;
    11. static {
    12. mapper = new ObjectMapper();
    13. mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
    14. false);
    15. }
    16. /**
    17. * 将对象序列化成json
    18. *
    19. * @param obj 待序列化的对象
    20. * @return
    21. * @throws Exception
    22. */
    23. public static String serialize(Object obj) throws Exception {
    24. if (obj == null) {
    25. throw new IllegalArgumentException("obj should not be null");
    26. }
    27. return mapper.writeValueAsString(obj);
    28. }
    29. /**
    30. 带泛型的反序列化,比如一个JSONArray反序列化成List<User>
    31. */
    32. public static <T> T deserialize(String jsonStr, Class<?> collectionClass,
    33. Class<?>... elementClasses) throws Exception {
    34. JavaType javaType = mapper.getTypeFactory().constructParametrizedType(
    35. collectionClass, collectionClass, elementClasses);
    36. return mapper.readValue(jsonStr, javaType);
    37. }
    38. /**
    39. * 将json字符串反序列化成对象
    40. * @param src 待反序列化的json字符串
    41. * @param t 反序列化成为的对象的class类型
    42. * @return
    43. * @throws Exception
    44. */
    45. public static <T> T deserialize(String src, Class<T> t) throws Exception {
    46. if (src == null) {
    47. throw new IllegalArgumentException("src should not be null");
    48. }
    49. if("{}".equals(src.trim())) {
    50. return null;
    51. }
    52. return mapper.readValue(src, t);
    53. }
    54. }

    具体使用缓存,和之前一样只需要关注@Cacheable和@CacheEvict注解,同样也支持spring的el表达式。而且这里的value属性表示的缓存名称也没有上面说的那个问题,完全可以用value隔离不同的缓存,例子如下:

    1. @Cacheable(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
    2. @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”;

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    摘录:Spring Boot 中使用自定义两级缓存的方法