Redis + Caffeine实现多级缓存

在项目中,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 [org.apache.ibatis.session.defaults.DefaultSqlSession@3b873190] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@820373569 wrapping com.mysql.cj.jdbc.ConnectionImpl@3865afb7] 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 [org.apache.ibatis.session.defaults.DefaultSqlSession@3b873190]
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,还不够完善,比如:如果要考虑并发,还应该在操作缓存的部分加锁,避免缓存击穿等问题。缓存工具类还可以封装的更加完善。

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值