当前位置:网站首页>Redis + Caffeine实现多级缓存
Redis + Caffeine实现多级缓存
2022-08-02 14:20:00 【风吹起海棠】
在项目中,MySQL不支持高性能的读写操作;为了进一步提升读写性能,引入缓存是必要的,如果只用Redis做一级缓存,可以结合spring-cache,基于注解的形式进行开发;但是如果想要加入多级缓存,就需要在每个方法里面加入多级缓存的相关代码,非常复杂,需要手写大量重复的缓存代码。
所以我采用 SpringAop + Redis + Caffeine + 自定义注解 + EL表达式 实现多级缓存的相关操作,这篇文章我会结合一个demon来分析
涉及的maven相关依赖:
<dependencies>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--日期时间工具-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${jodatime.version}</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- fastJson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>${commons-dbutils.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
</dependencies>
创建recl-cache模块:
Caffeine的配置类:
package com.ren.cache.config;
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;
/**
* @ClassName: CacheConfig
* @Description: TODO
* @Author: RZY
* @DATE: 2022/6/13 11:57
* @Version: v1.0
*/
@Configuration
public class CacheConfig {
@Bean
public Cache<String,Object> caffeineCache(){
return Caffeine.newBuilder()
.initialCapacity(128)//初始大小
.maximumSize(1024)//最大数量
.expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
.build();
}
}
Redis的配置类:
package com.ren.cache.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.ReadFrom;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashSet;
/**
* @Description: Redis配置类
* @ClassName: RedisConfig
* @Project_Name: recl
* @Author RZY
* @Date: 2021/9/20 09:34
* @Vertion: 2019.1
*/
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
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.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
自定义注解:
@MultiCache注解的定义:
package com.ren.cache.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiCache {
String cacheGroup(); //缓存分组
String prefixKey(); //缓存Key前缀
String key() default ""; //支持springEl表达式
long expireTime() default 120; //过期时间
CacheType type() default CacheType.FULL; //默认为存取缓存
}
CacheType枚举类:
package com.ren.cache.annotation;
public enum CacheType {
FULL, //存取
PUT, //只存
DELETE //删除
}
封装多级缓存的工具类:
package com.ren.cache.utils;
import com.github.benmanes.caffeine.cache.Cache;
import com.ren.cache.constant.CacheConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: CacheUtils
* @Description: TODO
* @Author: RZY
* @DATE: 2022/6/13 12:58
* @Version: v1.0
*/
@Component
@Slf4j
public class CacheUtils {
@Autowired
private Cache<String, Object> cache;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object getCache(String key) {
//从Caffeine中获取value,存在则直接返回
Object res = cache.getIfPresent(key);
if(Objects.nonNull(res)) {
log.info("--------从Caffeine中获取Key:" + res );
return res;
}
//redis中查询key
res = redisTemplate.opsForValue().get(key);
if(Objects.nonNull(res)) {
log.info("--------从Redis中获取Key:" + res );
//更新Caffeine中的Key
cache.put(key, res);
return res;
}
return null;
}
public void delCache(String key) {
//先删Caffeine中的Key
cache.invalidate(key);
log.info("--------Caffeine中删除Key成功:" + key );
//删除Redis中的Key
redisTemplate.delete(key);
log.info("--------Redis中删除Key成功:" + key );
}
public void putCache(String key, Object value, long timeOut) {
//更新Caffeine中的Key
cache.put(key, value);
//更新Redis的Key
redisTemplate.opsForValue().set(key, value, timeOut + new Random().nextInt(7), TimeUnit.SECONDS);
}
}
通过EL表达式获取需要的缓存参数:
package com.ren.cache.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.HashMap;
import java.util.TreeMap;
/**
* @ClassName: CacheEl
* @Description: TODO
* @Author: RZY
* @DATE: 2022/6/13 11:09
* @Version: v1.0
*/
public class CacheEl {
public static String parse(String elString, HashMap<String,Object> map){
elString=String.format("#{%s}",elString);
//创建表达式解析器
ExpressionParser parser = new SpelExpressionParser();
//通过evaluationContext.setVariable可以在上下文中设定变量。
EvaluationContext context = new StandardEvaluationContext();
map.forEach(context::setVariable);
//解析表达式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
//使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
return expression.getValue(context, String.class);
}
}
aop切面类的实现(环绕@MultiCache注解,只要加了该注解的方法,执行时就会被拦截):
package com.ren.eduservice.aop;
import com.ren.cache.annotation.CacheType;
import com.ren.cache.annotation.MultiCache;
import com.ren.cache.el.CacheEl;
import com.ren.cache.utils.CacheUtils;
import com.ren.utils.exceptionhandler.ReclException;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Objects;
/**
* @ClassName: CacheAspect
* @Description: TODO
* @Author: RZY
* @DATE: 2022/6/13 11:24
* @Version: v1.0
*/
@Component
@Aspect
public class CacheAspect {
@Autowired
CacheUtils cacheUtils;
@Pointcut("@annotation(com.ren.cache.annotation.MultiCache)")
public void cacheAspect() {
}
@Around("cacheAspect()")
public Object doCacheAround(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
MultiCache annotation = method.getAnnotation(MultiCache.class);
//定义el的key
String elKey = null;
//如果cache的key不为空,则解析参数
if(!annotation.key().equals("")) {
//获取方法参数
String[] parameterNames = signature.getParameterNames();
Object[] args = point.getArgs();
//放入map方便参数解释
HashMap<String, Object> map = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
map.put(parameterNames[i], args[i]);
}
elKey = CacheEl.parse(annotation.key(), map);
}
//生成最终的key
String finalKey = elKey == null ? annotation.cacheGroup() + "::" + annotation.prefixKey()
: annotation.cacheGroup() + "::" + annotation.prefixKey() + "::" + elKey;
//执行缓存key的查询
if(annotation.type() == CacheType.FULL) {
//从缓存中获取value并返回
Object res = cacheUtils.getCache(finalKey);
if(Objects.nonNull(res)) return res;
//缓存不存在查询db并放入缓存
try {
res = point.proceed();
} catch (Throwable e) {
throw new ReclException(20001, "查询db出错");
}
if(Objects.nonNull(res)) {
cacheUtils.putCache(finalKey, res, annotation.expireTime());
return res;
}
}
//执行缓存Key的更新操作
if(annotation.type() == CacheType.PUT) {
//更新数据库
Object res = null;
try {
res = point.proceed();
} catch (Throwable e) {
throw new ReclException(20001, "db更新出错");
}
//更新缓存
cacheUtils.putCache(finalKey, res, annotation.expireTime());
return res;
}
//执行缓存Key的删除操作
if(annotation.type() == CacheType.DELETE) {
//更新数据库
Object res = null;
try {
res = point.proceed();
} catch (Throwable e) {
throw new ReclException(20001, "db删除出错");
}
//更新缓存
cacheUtils.delCache(finalKey);
return res;
}
throw new ReclException(20001, "操作缓存失败");
}
}
使用案例:这是一个service方法,加入MultiCache注解,通过EL表达式可获取该方法的指定参数
@MultiCache(cacheGroup = "good",
prefixKey = CacheKeyPrefix.SINGLE_GOOD_ITEM,
key = "#id",
type = CacheType.FULL)
@Override
public ReclGood getGoodById(String id) {
return this.baseMapper.selectById(id);
}
运行项目,测试一下:
首先打开swagger测试获取某个id的商品信息:
第一次查询结果(查看日志):发现先查询了数据库,然后将缓存放入了Caffeine和Redis
Creating a new SqlSession
SqlSession [[email protected]] was not registered for synchronization because synchronization is not active
JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==> Preparing: SELECT id,good_name,good_price,good_code,good_discount,good_member_price,is_deleted,gmt_create,gmt_modified FROM recl_good WHERE id=? AND is_deleted=0
==> Parameters: 1536238288155774978(String)
<== Columns: id, good_name, good_price, good_code, good_discount, good_member_price, is_deleted, gmt_create, gmt_modified
<== Row: 1536238288155774978, 新奇士橙(330ml), 10.00, 67805508790041, 100, 9.50, 0, 2022-06-13 14:45:24, 2022-06-14 16:04:52
<== Total: 1
Closing non transactional SqlSession [[email protected]]
2022-07-11 15:27:49.986 INFO 15028 --- [nio-8080-exec-1] com.ren.cache.utils.CacheUtils : 成功将缓存放入Caffeine: Key good::single_good_item::1536238288155774978, value ReclGood(id=1536238288155774978, goodName=新奇士橙(330ml), goodPrice=10.00, goodCode=67805508790041, goodDiscount=100, goodMemberPrice=9.50, isDeleted=false, gmtCreate=Mon Jun 13 14:45:24 CST 2022, gmtModified=Tue Jun 14 16:04:52 CST 2022)
2022-07-11 15:27:50.042 INFO 15028 --- [nio-8080-exec-1] com.ren.cache.utils.CacheUtils : 成功将缓存放入Redis: Key good::single_good_item::1536238288155774978, value ReclGood(id=1536238288155774978, goodName=新奇士橙(330ml), goodPrice=10.00, goodCode=67805508790041, goodDiscount=100, goodMemberPrice=9.50, isDeleted=false, gmtCreate=Mon Jun 13 14:45:24 CST 2022, gmtModified=Tue Jun 14 16:04:52 CST 2022)
第二次查询结果(查看日志):快的不能再快了,直接Caffeine中拿到了缓存
2022-07-11 15:30:35.792 INFO 15028 --- [nio-8080-exec-4] com.ren.cache.utils.CacheUtils : --------从Caffeine中获取Key:ReclGood(id=1536238288155774978, goodName=新奇士橙(330ml), goodPrice=10.00, goodCode=67805508790041, goodDiscount=100, goodMemberPrice=9.50, isDeleted=false, gmtCreate=Mon Jun 13 14:45:24 CST 2022, gmtModified=Tue Jun 14 16:04:52 CST 2022)
最后连接redis客服端查看这个key:
总结:只展示了查询操作,删除等操作同理。这是一个小demon,还不够完善,比如:如果要考虑并发,还应该在操作缓存的部分加锁,避免缓存击穿等问题。缓存工具类还可以封装的更加完善。
边栏推荐
猜你喜欢
随机推荐
UINIX 高级环境编程杂项之限制
golang中使用泛型
详解C语言中的位操作运算符可以怎么用?
IDEA如何进行远程Debug
基于Visual Studio 2015的CUDA编程(一):基本配置
DOM - Event Object
Mediasoup 杂谈(待完善)
js中的join()方法
从零开始的循环之旅(下)
CUDA programming based on Visual Studio 2015 (1): basic configuration
移除元素 - 双指针
小知识系列:Fork之后如何与原仓库分支同步
Servlet 技术1
DOM - page rendering process
test3
Mysql索引底层数据结构
网络运维系列:二级域名启用与配置
Filter 过滤器
网络运维系列:远程服务器登录、配置与管理
idea使用jdbc对数据库进行增删改查,以及使用懒汉方式实现单例模式