在实际的项目中,我们通常会将一些热点数据存储到Redis
或MemCache
这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。
在一些场景下可能还需要进一步配合本地缓存使用,例如Guava cache
或Caffeine
,从而再次提升程序的响应速度与服务性能。
于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构。
二级缓存的访问流程可以用下面这张图来表示:
优点与问题
准备工作
<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-data-redis</artifactId>
</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>
在application.yml
中配置Redis
的连接信息:
spring:redis:host: 127.0.0.1port: 6379database: 0timeout: 10000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0
我们使用RedisTemplate
来对redis
进行读写操作。
下面在单机环境下,将按照对业务侵入性的不同程度,分三个版本来实现两级缓存的使用。
V1.0版本
在使用Cache
前,需要先配置一下相关参数:
@Configuration
public class CaffeineConfig {@Beanpublic Cache<String,Object> caffeineCache(){return Caffeine.newBuilder().initialCapacity(128)//初始大小.maximumSize(1024)//最大数量.expireAfterWrite(60, TimeUnit.SECONDS)//过期时间.build();}
}
@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService {private final OrderMapper orderMapper;@Overridepublic Order getOrderById(Long id) { Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id)); return order;}@Overridepublic void updateOrder(Order order) { orderMapper.updateById(order);}@Overridepublic void deleteOrder(Long id) {orderMapper.deleteById(id);}
}
接下来,对上面的OrderService
进行改造,在执行正常业务外再加上操作两级缓存的代码,先看改造后的查询操作:
public Order getOrderById(Long id) {String key = CacheConstant.ORDER + id;Order order = (Order) cache.get(key,k -> {//先查询 RedisObject obj = redisTemplate.opsForValue().get(k);if (Objects.nonNull(obj)) {log.info("get data from redis");return obj;}// Redis没有则查询 DBlog.info("get data from database");Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));redisTemplate.opsForValue().set(k, myOrder, 120, TimeUnit.SECONDS);return myOrder;});return order;
}
public void updateOrder(Order order) {log.info("update order data");String key=CacheConstant.ORDER + order.getId();orderMapper.updateById(order);//修改 RedisredisTemplate.opsForValue().set(key,order,120, TimeUnit.SECONDS);// 修改本地缓存cache.put(key,order);
}
看一下下面图中接口的调用、以及缓存的刷新过程。可以看到在更新数据后,同步刷新了缓存中的内容,再之后的访问接口时不查询数据库,也可以拿到正确的结果:
最后再来看一下删除操作,在删除数据的同时,手动移除Reids
和Caffeine
中的缓存:
public void deleteOrder(Long id) {log.info("delete order");orderMapper.deleteById(id);String key= CacheConstant.ORDER + id;redisTemplate.delete(key);cache.invalidate(key);
}
我们在删除某个缓存后,再次调用之前的查询接口时,又会出现重新查询数据库的情况:
V2.0版本
@Configuration
public class CacheManagerConfig {@Beanpublic CacheManager cacheManager(){CaffeineCacheManager cacheManager=new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(128).maximumSize(1024).expireAfterWrite(60, TimeUnit.SECONDS));return cacheManager;}
}
@Cacheable(value = "order",key = "#id")
//@Cacheable(cacheNames = "order",key = "#p0")
public Order getOrderById(Long id) {String key= CacheConstant.ORDER + id;//先查询 RedisObject obj = redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)){log.info("get data from redis");return (Order) obj;}// Redis没有则查询 DBlog.info("get data from database");Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));redisTemplate.opsForValue().set(key,myOrder,120, TimeUnit.SECONDS);return myOrder;
}
#参数名
#参数对象.属性名
#p参数对应下标
@CachePut(cacheNames = "order",key = "#order.id")
public Order updateOrder(Order order) {log.info("update order data");orderMapper.updateById(order);//修改 RedisredisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),order, 120, TimeUnit.SECONDS);return order;
}
@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {log.info("delete order");orderMapper.deleteById(id);redisTemplate.delete(CacheConstant.ORDER + id);
}
V3.0版本
模仿spring
通过注解管理缓存的方式,我们也可以选择自定义注解,然后在切面中处理缓存,从而将对业务代码的入侵降到最低。
首先定义一个注解,用于添加在需要操作缓存的方法上:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {String cacheName();String key(); //支持springEl表达式long l2TimeOut() default 120;CacheType type() default CacheType.FULL;
}
我们使用cacheName + key
作为缓存的真正key
(仅存在一个Cache
中,不做CacheName
隔离),l2TimeOut
为可以设置的二级缓存Redis
的过期时间,type
是一个枚举类型的变量,表示操作缓存的类型,枚举类型定义如下:
public enum CacheType {FULL, //存取PUT, //只存DELETE //删除
}
因为要使key
支持springEl
表达式,所以需要写一个方法,使用表达式解析器解析参数:
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;
}
参数中的elString
对应的就是注解中key
的值,map
是将原方法的参数封装后的结果。简单进行一下测试:
public void test() {String elString="#order.money";String elString2="#user";String elString3="#p0"; TreeMap<String,Object> map=new TreeMap<>();Order order = new Order();order.setId(111L);order.setMoney(123D);map.put("order",order);map.put("user","Hydra");String val = parse(elString, map);String val2 = parse(elString2, map);String val3 = parse(elString3, map);System.out.println(val);System.out.println(val2);System.out.println(val3);
}
执行结果如下,可以看到支持按照参数名称、参数对象的属性名称读取,但是不支持按照参数下标读取,暂时留个小坑以后再处理。
123.0
Hydra
null
至于Cache
相关参数的配置,我们沿用V1版本中的配置即可。准备工作做完了,下面我们定义切面,在切面中操作Cache
来读写Caffeine
的缓存,操作RedisTemplate
读写Redis
缓存。
@Slf4j @Component @Aspect
@AllArgsConstructor
public class CacheAspect {private final Cache cache;private final RedisTemplate redisTemplate;@Pointcut("@annotation(com.cn.dc.annotation.DoubleCache)")public void cacheAspect() {}@Around("cacheAspect()")public Object doAround(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();//拼接解析springEl表达式的mapString[] 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]);}DoubleCache annotation = method.getAnnotation(DoubleCache.class);String elResult = ElParser.parse(annotation.key(), treeMap);String realKey = annotation.cacheName() + CacheConstant.COLON + elResult;//强制更新if (annotation.type()== CacheType.PUT){Object object = point.proceed();redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);cache.put(realKey, object);return object;}//删除else if (annotation.type()== CacheType.DELETE){redisTemplate.delete(realKey);cache.invalidate(realKey);return point.proceed();}//读写,查询CaffeineObject caffeineCache = cache.getIfPresent(realKey);if (Objects.nonNull(caffeineCache)) {log.info("get data from caffeine");return caffeineCache;}//查询RedisObject redisCache = redisTemplate.opsForValue().get(realKey);if (Objects.nonNull(redisCache)) {log.info("get data from redis");cache.put(realKey, redisCache);return redisCache;}log.info("get data from database");Object object = point.proceed();if (Objects.nonNull(object)){//写入RedisredisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);//写入Caffeinecache.put(realKey, object); }return object;}
}
@DoubleCache(cacheName = "order", key = "#id",type = CacheType.FULL)
public Order getOrderById(Long id) {Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));return myOrder;
}@DoubleCache(cacheName = "order",key = "#order.id",type = CacheType.PUT)
public Order updateOrder(Order order) {orderMapper.updateById(order);return order;
}@DoubleCache(cacheName = "order",key = "#id",type = CacheType.DELETE)
public void deleteOrder(Long id) {orderMapper.deleteById(id);
}
到这里,基于切面操作缓存的改造就完成了,Service
的代码也瞬间清爽了很多,让我们可以继续专注于业务逻辑处理,而不用费心去操作两级缓存了。
总结
本文按照对业务入侵的递减程度,依次介绍了三种管理两级缓存的方法。
本文中只是介绍了最基础的使用,实际中的并发问题、事务的回滚问题都需要考虑,还需要思考什么数据适合放在一级缓存、什么数据适合放在二级缓存等等的其他问题。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!