当前位置:   article > 正文

Springboot简单设计两级缓存_springboot二级缓存

springboot二级缓存

两级缓存相比单纯使用远程缓存,具有什么优势呢?
本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度
使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时
但是在设计中,还是要考虑一些问题的,例如数据一致性问题。首先,两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。
如果是分布式环境下,一级缓存之间也会存在一致性问题,当一个节点下的本地缓存修改后,需要通知其他节点也刷新本地缓存中的数据,否则会出现读取到过期数据的情况,这一问题可以通过类似于Redis中的发布/订阅功能解决。
此外,缓存的过期时间、过期策略以及多线程访问的问题也都需要考虑进去,不过我们今天暂时先不考虑这些问题,简单的在代码中实现两级缓存的管理。

图片中一级缓存找的图是Ehcache,但实际项目中我使用的是caffeine做一级缓存,redis做二级缓存原理都是一样,先引入相关依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.github.ben-manes.caffeine</groupId>
  7. <artifactId>caffeine</artifactId>
  8. <version>2.9.2</version>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-cache</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.apache.commons</groupId>
  16. <artifactId>commons-pool2</artifactId>
  17. <version>2.8.1</version>
  18. </dependency>

在yml中配置redis的相关信息

  1. redis:
  2. host: localhost
  3. port: 6379
  4. password:
  5. timeout: 5000
  6. # lettuce:
  7. # pool:
  8. # max-active: 8
  9. # max-wait: -1ms
  10. # max-idle: 8
  11. # min-idle: 0

注释的lettuce都是默认值,实际要调整放开注释自行调整即可

枚举类型枚举

  1. public enum CacheType {
  2. /**
  3. * 存取
  4. */
  5. FULL,
  6. /**
  7. * 只存
  8. */
  9. PUT,
  10. /**
  11. * 删除
  12. */
  13. DEL
  14. }

定义一个注解,用于添加在需要操作缓存的方法上,使用cacheName + key作为缓存的真正key,timeOut为可以设置的二级缓存Redis的过期时间,type是一个枚举类型的变量,表示操作缓存的类型

  1. import com.yx.light.element.jpa.enums.CacheType;
  2. import java.lang.annotation.*;
  3. @Target(ElementType.METHOD)
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface L2Cache {
  7. /**
  8. * 缓存名字
  9. * @return
  10. */
  11. String cacheName();
  12. /**
  13. * 缓存key
  14. * @return
  15. */
  16. String key() default ""; //支持springEl表达式
  17. /**
  18. * redis缓存超时时间
  19. * @return
  20. */
  21. long timeOut() default 120;
  22. /**
  23. * 缓存类型
  24. * @return
  25. */
  26. CacheType type() default CacheType.FULL;
  27. }

RedisTemplate配置类

  1. import org.springframework.cache.annotation.CachingConfigurerSupport;
  2. import org.springframework.cache.annotation.EnableCaching;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.data.redis.connection.RedisConnectionFactory;
  6. import org.springframework.data.redis.core.RedisTemplate;
  7. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
  8. import org.springframework.data.redis.serializer.StringRedisSerializer;
  9. @Configuration
  10. @EnableCaching
  11. public class RedisConfig extends CachingConfigurerSupport {
  12. /**
  13. * 配置自定义redisTemplate
  14. *
  15. * @return
  16. */
  17. @Bean
  18. RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  19. RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  20. redisTemplate.setConnectionFactory(redisConnectionFactory);
  21. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  22. //设置值(value)的序列化采用Jackson2JsonRedisSerializer。
  23. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  24. //设置键(key)的序列化采用StringRedisSerializer。
  25. redisTemplate.setKeySerializer(new StringRedisSerializer());
  26. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  27. redisTemplate.afterPropertiesSet();
  28. return redisTemplate;
  29. }
  30. }

Caffeine配置类

  1. import com.github.benmanes.caffeine.cache.Cache;
  2. import com.github.benmanes.caffeine.cache.Caffeine;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import java.util.concurrent.TimeUnit;
  6. @Configuration
  7. public class CaffeineConfig {
  8. @Bean
  9. public Cache<String, Object> caffeineCache() {
  10. return Caffeine.newBuilder()
  11. .initialCapacity(128)//初始大小
  12. .maximumSize(1024)//最大数量
  13. .expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
  14. .build();
  15. }
  16. }

El转换辅助工具类

  1. import org.springframework.expression.EvaluationContext;
  2. import org.springframework.expression.Expression;
  3. import org.springframework.expression.ExpressionParser;
  4. import org.springframework.expression.common.TemplateParserContext;
  5. import org.springframework.expression.spel.standard.SpelExpressionParser;
  6. import org.springframework.expression.spel.support.StandardEvaluationContext;
  7. import java.util.TreeMap;
  8. public class ElParserUtil {
  9. private ElParserUtil() {
  10. }
  11. public static String parse(String elString, TreeMap<String, Object> map) {
  12. elString = String.format("#{%s}", elString);
  13. //创建表达式解析器
  14. ExpressionParser parser = new SpelExpressionParser();
  15. //通过evaluationContext.setVariable可以在上下文中设定变量。
  16. EvaluationContext context = new StandardEvaluationContext();
  17. map.entrySet().forEach(entry ->
  18. context.setVariable(entry.getKey(), entry.getValue())
  19. );
  20. //解析表达式
  21. Expression expression = parser.parseExpression(elString, new TemplateParserContext());
  22. //使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
  23. String value = expression.getValue(context, String.class);
  24. return value;
  25. }
  26. }

两级缓存切面,在切面中操作Cache来读写Caffeine的缓存,操作RedisTemplate读写Redis缓存。

  1. import com.github.benmanes.caffeine.cache.Cache;
  2. import com.yx.light.element.jpa.annotations.L2Cache;
  3. import com.yx.light.element.jpa.enums.CacheType;
  4. import com.yx.light.element.jpa.utils.ElParserUtil;
  5. import lombok.AllArgsConstructor;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.aspectj.lang.ProceedingJoinPoint;
  8. import org.aspectj.lang.annotation.Around;
  9. import org.aspectj.lang.annotation.Aspect;
  10. import org.aspectj.lang.annotation.Pointcut;
  11. import org.aspectj.lang.reflect.MethodSignature;
  12. import org.springframework.data.redis.core.RedisTemplate;
  13. import org.springframework.stereotype.Component;
  14. import org.springframework.util.StringUtils;
  15. import java.lang.reflect.Method;
  16. import java.util.Objects;
  17. import java.util.TreeMap;
  18. import java.util.concurrent.TimeUnit;
  19. @Slf4j
  20. @Component
  21. @Aspect
  22. @AllArgsConstructor
  23. public class L2CacheAspect {
  24. private final Cache cache;
  25. private final RedisTemplate redisTemplate;
  26. @Pointcut("@annotation(com.yx.light.element.jpa.annotations.L2Cache)")
  27. public void cacheAspect() {
  28. }
  29. @Around("cacheAspect()")
  30. public Object doAround(ProceedingJoinPoint point) throws Throwable {
  31. MethodSignature signature = (MethodSignature) point.getSignature();
  32. Method method = signature.getMethod();
  33. //拼接解析springEl表达式的map
  34. String[] paramNames = signature.getParameterNames();
  35. Object[] args = point.getArgs();
  36. TreeMap<String, Object> treeMap = new TreeMap<>();
  37. for (int i = 0; i < paramNames.length; i++) {
  38. treeMap.put(paramNames[i], args[i]);
  39. }
  40. L2Cache annotation = method.getAnnotation(L2Cache.class);
  41. String elResult = null;
  42. if (treeMap.size() == 0) {
  43. elResult = StringUtils.isEmpty(annotation.key()) ? method.getName() : annotation.key();
  44. } else {
  45. elResult = ElParserUtil.parse(annotation.key(), treeMap);
  46. }
  47. String realKey = annotation.cacheName() + ":" + elResult;
  48. //强制更新
  49. if (annotation.type() == CacheType.PUT) {
  50. Object object = point.proceed();
  51. if (Objects.isNull(object)) {
  52. log.info("方法执行完毕无返回值,无需更新缓存");
  53. return object;
  54. }
  55. redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
  56. cache.put(realKey, object);
  57. return object;
  58. } else if (annotation.type() == CacheType.DEL) {
  59. redisTemplate.delete(realKey);
  60. cache.invalidate(realKey);
  61. return point.proceed();
  62. }
  63. //读写,查询Caffeine
  64. Object caffeineCache = cache.getIfPresent(realKey);
  65. if (Objects.nonNull(caffeineCache)) {
  66. log.info("从caffeine中获取数据");
  67. return caffeineCache;
  68. }
  69. //查询Redis
  70. Object redisCache = redisTemplate.opsForValue().get(realKey);
  71. if (Objects.nonNull(redisCache)) {
  72. log.info("从redis中获取数据");
  73. cache.put(realKey, redisCache);
  74. return redisCache;
  75. }
  76. log.info("从数据库中获取数据");
  77. Object object = point.proceed();
  78. if (Objects.nonNull(object)) {
  79. //写入Redis
  80. redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
  81. //写入Caffeine
  82. cache.put(realKey, object);
  83. }
  84. return object;
  85. }
  86. }

改造service实现类的几个方法简单测试一下

  1. @Override
  2. @L2Cache(cacheName = "GroupHeader", type = CacheType.FULL)
  3. public List<GroupHeader> findAllGroupHeader() {
  4. return groupHeaderRepository.findAll();
  5. }
  6. @Override
  7. @L2Cache(cacheName = "GroupHeader", key = "#groupHeader.groupCode", type = CacheType.PUT)
  8. public void editGroupHeader(GroupHeader groupHeader) {
  9. groupHeaderRepository.save(groupHeader);
  10. }
  11. @Override
  12. @L2Cache(cacheName = "GroupHeader", key = "#ids", type = CacheType.DEL)
  13. public void deleteGroupHeader(String ids) {
  14. String[] split = ids.split(",");
  15. for (String id : split) {
  16. groupHeaderRepository.deleteById(Long.parseLong(id));
  17. }
  18. }

连续调用两次查询接口看日志打印效果和redis客户端的查询

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/793723
推荐阅读
相关标签
  

闽ICP备14008679号