赞
踩
两级缓存相比单纯使用远程缓存,具有什么优势呢?
本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度
使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时
但是在设计中,还是要考虑一些问题的,例如数据一致性问题。首先,两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。
如果是分布式环境下,一级缓存之间也会存在一致性问题,当一个节点下的本地缓存修改后,需要通知其他节点也刷新本地缓存中的数据,否则会出现读取到过期数据的情况,这一问题可以通过类似于Redis中的发布/订阅功能解决。
此外,缓存的过期时间、过期策略以及多线程访问的问题也都需要考虑进去,不过我们今天暂时先不考虑这些问题,简单的在代码中实现两级缓存的管理。
图片中一级缓存找的图是Ehcache,但实际项目中我使用的是caffeine做一级缓存,redis做二级缓存原理都是一样,先引入相关依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <dependency>
- <groupId>com.github.ben-manes.caffeine</groupId>
- <artifactId>caffeine</artifactId>
- <version>2.9.2</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-pool2</artifactId>
- <version>2.8.1</version>
- </dependency>
在yml中配置redis的相关信息
- redis:
- host: localhost
- port: 6379
- password:
- timeout: 5000
- # lettuce:
- # pool:
- # max-active: 8
- # max-wait: -1ms
- # max-idle: 8
- # min-idle: 0
注释的lettuce都是默认值,实际要调整放开注释自行调整即可
枚举类型枚举
- public enum CacheType {
-
- /**
- * 存取
- */
- FULL,
-
- /**
- * 只存
- */
- PUT,
-
- /**
- * 删除
- */
- DEL
- }
定义一个注解,用于添加在需要操作缓存的方法上,使用cacheName + key作为缓存的真正key,timeOut为可以设置的二级缓存Redis的过期时间,type是一个枚举类型的变量,表示操作缓存的类型
- import com.yx.light.element.jpa.enums.CacheType;
-
- import java.lang.annotation.*;
-
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface L2Cache {
-
- /**
- * 缓存名字
- * @return
- */
- String cacheName();
-
- /**
- * 缓存key
- * @return
- */
- String key() default ""; //支持springEl表达式
-
- /**
- * redis缓存超时时间
- * @return
- */
- long timeOut() default 120;
-
- /**
- * 缓存类型
- * @return
- */
- CacheType type() default CacheType.FULL;
- }
RedisTemplate配置类
- import org.springframework.cache.annotation.CachingConfigurerSupport;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- @Configuration
- @EnableCaching
- public class RedisConfig extends CachingConfigurerSupport {
-
- /**
- * 配置自定义redisTemplate
- *
- * @return
- */
- @Bean
- RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
- RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
- //设置值(value)的序列化采用Jackson2JsonRedisSerializer。
- redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
- //设置键(key)的序列化采用StringRedisSerializer。
- redisTemplate.setKeySerializer(new StringRedisSerializer());
- redisTemplate.setHashKeySerializer(new StringRedisSerializer());
- redisTemplate.afterPropertiesSet();
- return redisTemplate;
- }
- }
Caffeine配置类
- import com.github.benmanes.caffeine.cache.Cache;
- import com.github.benmanes.caffeine.cache.Caffeine;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- import java.util.concurrent.TimeUnit;
-
- @Configuration
- public class CaffeineConfig {
-
- @Bean
- public Cache<String, Object> caffeineCache() {
- return Caffeine.newBuilder()
- .initialCapacity(128)//初始大小
- .maximumSize(1024)//最大数量
- .expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
- .build();
- }
- }
El转换辅助工具类
- import org.springframework.expression.EvaluationContext;
- import org.springframework.expression.Expression;
- import org.springframework.expression.ExpressionParser;
- import org.springframework.expression.common.TemplateParserContext;
- import org.springframework.expression.spel.standard.SpelExpressionParser;
- import org.springframework.expression.spel.support.StandardEvaluationContext;
-
- import java.util.TreeMap;
-
- public class ElParserUtil {
-
- private ElParserUtil() {
- }
-
- public static String parse(String elString, TreeMap<String, Object> map) {
- elString = String.format("#{%s}", elString);
- //创建表达式解析器
- ExpressionParser parser = new SpelExpressionParser();
- //通过evaluationContext.setVariable可以在上下文中设定变量。
- EvaluationContext context = new StandardEvaluationContext();
- map.entrySet().forEach(entry ->
- context.setVariable(entry.getKey(), entry.getValue())
- );
-
- //解析表达式
- Expression expression = parser.parseExpression(elString, new TemplateParserContext());
- //使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
- String value = expression.getValue(context, String.class);
- return value;
- }
- }
两级缓存切面,在切面中操作Cache来读写Caffeine的缓存,操作RedisTemplate读写Redis缓存。
- import com.github.benmanes.caffeine.cache.Cache;
- import com.yx.light.element.jpa.annotations.L2Cache;
- import com.yx.light.element.jpa.enums.CacheType;
- import com.yx.light.element.jpa.utils.ElParserUtil;
- import lombok.AllArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- 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.springframework.data.redis.core.RedisTemplate;
- import org.springframework.stereotype.Component;
- import org.springframework.util.StringUtils;
-
- import java.lang.reflect.Method;
- import java.util.Objects;
- import java.util.TreeMap;
- import java.util.concurrent.TimeUnit;
-
- @Slf4j
- @Component
- @Aspect
- @AllArgsConstructor
- public class L2CacheAspect {
-
- private final Cache cache;
- private final RedisTemplate redisTemplate;
-
- @Pointcut("@annotation(com.yx.light.element.jpa.annotations.L2Cache)")
- public void cacheAspect() {
- }
-
- @Around("cacheAspect()")
- public Object doAround(ProceedingJoinPoint point) throws Throwable {
- MethodSignature signature = (MethodSignature) point.getSignature();
- Method method = signature.getMethod();
-
- //拼接解析springEl表达式的map
- String[] paramNames = signature.getParameterNames();
- Object[] args = point.getArgs();
- TreeMap<String, Object> treeMap = new TreeMap<>();
- for (int i = 0; i < paramNames.length; i++) {
- treeMap.put(paramNames[i], args[i]);
- }
-
- L2Cache annotation = method.getAnnotation(L2Cache.class);
- String elResult = null;
- if (treeMap.size() == 0) {
- elResult = StringUtils.isEmpty(annotation.key()) ? method.getName() : annotation.key();
- } else {
- elResult = ElParserUtil.parse(annotation.key(), treeMap);
- }
- String realKey = annotation.cacheName() + ":" + elResult;
-
- //强制更新
- if (annotation.type() == CacheType.PUT) {
- Object object = point.proceed();
- if (Objects.isNull(object)) {
- log.info("方法执行完毕无返回值,无需更新缓存");
- return object;
- }
- redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
- cache.put(realKey, object);
- return object;
- } else if (annotation.type() == CacheType.DEL) {
- redisTemplate.delete(realKey);
- cache.invalidate(realKey);
- return point.proceed();
- }
-
- //读写,查询Caffeine
- Object caffeineCache = cache.getIfPresent(realKey);
- if (Objects.nonNull(caffeineCache)) {
- log.info("从caffeine中获取数据");
- return caffeineCache;
- }
-
- //查询Redis
- Object redisCache = redisTemplate.opsForValue().get(realKey);
- if (Objects.nonNull(redisCache)) {
- log.info("从redis中获取数据");
- cache.put(realKey, redisCache);
- return redisCache;
- }
-
- log.info("从数据库中获取数据");
- Object object = point.proceed();
- if (Objects.nonNull(object)) {
- //写入Redis
- redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
- //写入Caffeine
- cache.put(realKey, object);
- }
- return object;
- }
- }
改造service实现类的几个方法简单测试一下
- @Override
- @L2Cache(cacheName = "GroupHeader", type = CacheType.FULL)
- public List<GroupHeader> findAllGroupHeader() {
- return groupHeaderRepository.findAll();
- }
-
- @Override
- @L2Cache(cacheName = "GroupHeader", key = "#groupHeader.groupCode", type = CacheType.PUT)
- public void editGroupHeader(GroupHeader groupHeader) {
- groupHeaderRepository.save(groupHeader);
- }
-
- @Override
- @L2Cache(cacheName = "GroupHeader", key = "#ids", type = CacheType.DEL)
- public void deleteGroupHeader(String ids) {
- String[] split = ids.split(",");
- for (String id : split) {
- groupHeaderRepository.deleteById(Long.parseLong(id));
- }
- }
连续调用两次查询接口看日志打印效果和redis客户端的查询
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。