一不小心,弄了一个开源组件:caffeine+redis的多级缓存框架

大家好,我是小义,今天介绍一下如何构建一个基于springboot实现的自定义starter组件,即一个可灵活配置过期时间的多级缓存框架。

组件化是SpringBoot一大优点。 starter组件是Spring Boot生态系统的一部分,它们帮助开发者快速搭建项目,减少配置的复杂性,并且确保依赖管理的一致性。开发者可以根据自己的需求选择合适的starter来集成到项目中。

尽管各种starters在实现细节上各具特色,但它们普遍遵循两个核心原则:配置属性(ConfigurationProperties)和自动配置(AutoConfiguration)。这一设计哲学源自Spring Boot所倡导的“约定优于配置”(Convention Over Configuration)的理念。

一个简单的starter项目应包含以下目录。

demo-spring-boot-starter
  |-src
    |-main
      |-java
        |-xxx.xxx 
          |-DemoConfig.java
      |-resource
        |-META-INF
          |-spring.factories
        |-application.properties
  |-pom.xml

自定义starter的步骤如下:

  1. 定义坐标:创建一个新的Maven项目,并定义其坐标(groupId, artifactId, version)。
  2. 编写自动配置类:创建一个带有@Configuration注解的类,实现自定义配置。
  3. 创建META-INF/spring.factories文件:指定自动配置类,让Spring Boot识别。
  4. 打包和发布:将starter下载到本地或发布到Maven仓库。

下面来一一实现。

首先,在spring.factories中,我们指定一下要自动装配的配置类,这样就可以将设置的包扫描路径下的相关bean部署到SpringBoot 中。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xiaoyi.multiTtlCache.config.CustomizedRedisAutoConfiguration

在pom.xml中,需要引入autoconfigure自动装配的maven依赖包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>${spring-boot.version}</version>
</dependency>

<!--spring-boot-configuration-processor的作用是生成配置的元数据信息,即META-INF目录下的spring-configuration-metadata.json文件,从而告诉spring这个jar包中有哪些自定义的配置-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>${spring-boot.version}</version>
</dependency>

先设置缓存配置类,类名和spring.factories中的对应上。这其中涉及到本地缓存caffeine和redis缓存配置,关于caffeine的相关内容可以看之前的文章。

@Configuration
@Import(SpringUtil.class)
@ComponentScan(basePackages = "com.xiaoyi.multiTtlCache")
@EnableCaching
public class CustomizedRedisAutoConfiguration {

    public static final String REDISTEMPLATE_BEAN_NAME = "cacheRedisTemplate";

    @Bean(REDISTEMPLATE_BEAN_NAME)
    public RedisTemplate<String, Object> cacheRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        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);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public MyCaffeineCache myCaffeineCache() {
        MyCaffeineCache myCaffeineCache = new MyCaffeineCache();
        myCaffeineCache.init();
        return myCaffeineCache;
    }
}

创建一个自定义注解类,添加ttl等时间设置属性。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomizedCacheable {


    String[] value() default {};


    String[] cacheNames() default {};


    String key() default "";


    String keyGenerator() default "";


    String cacheResolver() default "";


    String condition() default "";


    String unless() default "";


    boolean sync() default false;

    //String cacheManager() default "redisCacheManager";

    /**
     * 过期时间
     * @return
     */
    long expiredTimeSecond() default 0;

    /**
     * 预刷新时间
     * @return
     */
    long preLoadTimeSecond() default 0;

    /**
     * 缓存级别,1-本地缓存,2-redis缓存,3-本地+redis
     * @return
     */
    String cacheType() default "1";

    /**
     * 一二级缓存之间的缓存过期时间差
     * @return
     */
    long expiredInterval() default 0;

    /**
     * 是否开启缓存
     * @return
     */
    String cacheEnabled() default "1";

    long test() default 1;
}

增加自定义注解的拦截器,根据设置的缓存等级决定走本地缓存还是redis缓存,同时比较缓存的剩余过期时间是否小于阈值(preLoadTimeSecond),小于则重新刷新缓存,达到缓存预热的效果,同时减少缓存击穿的问题。
核心代码如下:

@Component
@Aspect
@Slf4j
@Order(1)
public class CacheReloadAspect {

    @Autowired
    private Environment environment;

    @Autowired
    private ApplicationContext applicationContext;

    private ReentrantLock lock = new ReentrantLock();

    @SneakyThrows
    @Around(value = "@annotation(com.xiaoyi.multiTtlCache.annotation.CustomizedCacheable)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint){
        //方法入参对象数组
        Object[] args = proceedingJoinPoint.getArgs();
        //方法实体
        Method method = MethodSignature.class.cast(proceedingJoinPoint.getSignature()).getMethod();
        //自定义注解
        CustomizedCacheable cacheable = method.getAnnotation(CustomizedCacheable.class);
        String cacheEnabled = cacheable.cacheEnabled();
        //根据配置判断是否开启缓存
        String property = environment.getProperty(cacheEnabled);
        if (!ObjectUtil.isEmpty(property)) {
            return proceedingJoinPoint.proceed();
        }
        //解析上下文
        StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext();
        //参数名称
        String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
        for (int i = 0; i < parameterNames.length; i++) {
            standardEvaluationContext.setVariable(parameterNames[i], args[i]);
        }
        //解析SPEL表达式的key,获取真正存入缓存中的key值
        String key = parseSPELKey(cacheable, standardEvaluationContext);
        Object result = null;
        String cacheType = cacheable.cacheType();
        switch (cacheType) {
            case CacheConstant.LOCAL_CACHE:
                result = useCaffeineCache(key, cacheable, proceedingJoinPoint);
            case CacheConstant.REDIS_CACHE:
                result = useRedisCache(key, cacheable, proceedingJoinPoint);
            case CacheConstant.BOTH_CACHE:
                result = useBothCache(key, cacheable, proceedingJoinPoint);
            default:
                result = null;
        }
        return result;

    }

    @SneakyThrows
    private Object useBothCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {
        long expiredInterval = cacheable.expiredInterval();
        MyCaffeineCache myCaffeineCache = (MyCaffeineCache) SpringUtil.getBean(MyCaffeineCache.class);
        RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("cacheRedisTemplate");
        Object o = myCaffeineCache.get(key);
        if (o != null) {
            Long ttl = myCaffeineCache.getTtl(key);
            if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){
                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",key,ttl,cacheable.preLoadTimeSecond());
                ThreadUtil.execute(()->{
                    lock.lock();
                    try{
                        CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);
                        Object o1 = CacheHelper.exeInvocation(cachedInvocation);
                        myCaffeineCache.set(key, o1, cacheable.expiredTimeSecond());
                        redisTemplate.opsForValue().set(key, o1, cacheable.expiredTimeSecond() + expiredInterval, TimeUnit.SECONDS);
                    }catch (Exception e){
                        log.error("{}",e.getMessage(),e);
                    }finally {
                        lock.unlock();
                    }
                });
            }
            return o;
        } else {
            Object o1 = redisTemplate.opsForValue().get(key);
            Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            if(o1 != null){
                myCaffeineCache.set(key, o1, ttl);
                return o1;
            }
        }
        Object result = proceedingJoinPoint.proceed();
        myCaffeineCache.set(key, result, cacheable.expiredTimeSecond());
        redisTemplate.opsForValue().set(key, result, cacheable.expiredTimeSecond() + expiredInterval, TimeUnit.SECONDS);
        return result;
    }

    @SneakyThrows
    private Object useRedisCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {
        RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("cacheRedisTemplate");
        Object o = redisTemplate.opsForValue().get(key);
        if (o != null) {
            Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){
                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}", key, ttl, cacheable.preLoadTimeSecond());
                ThreadUtil.execute(()->{
                    lock.lock();
                    try{
                        CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);
                        Object o1 = CacheHelper.exeInvocation(cachedInvocation);
                        redisTemplate.opsForValue().set(key, o1, cacheable.expiredTimeSecond(), TimeUnit.SECONDS);
                    }catch (Exception e){
                        log.error("{}",e.getMessage(),e);
                    }finally {
                        lock.unlock();
                    }
                });
            }
            return o;
        }
        Object result = proceedingJoinPoint.proceed();
        redisTemplate.opsForValue().set(key, result, cacheable.expiredTimeSecond(), TimeUnit.SECONDS);
        return result;
    }

    @SneakyThrows
    private Object useCaffeineCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {
        MyCaffeineCache myCaffeineCache = (MyCaffeineCache) SpringUtil.getBean(MyCaffeineCache.class);
        Object o = myCaffeineCache.get(key);
        if (o != null) {
            Long ttl = myCaffeineCache.getTtl(key);
            if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){
                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",key,ttl,cacheable.preLoadTimeSecond());
                ThreadUtil.execute(()->{
                    lock.lock();
                    try{
                        CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);
                        Object o1 = CacheHelper.exeInvocation(cachedInvocation);
                        myCaffeineCache.set(key, o1, cacheable.expiredTimeSecond());
                    }catch (Exception e){
                        log.error("{}",e.getMessage(),e);
                    }finally {
                        lock.unlock();
                    }
                });
            }
            return o;
        }
        Object result = proceedingJoinPoint.proceed();
        myCaffeineCache.set(key, result, cacheable.expiredTimeSecond());
        return result;
    }

    private CachedInvocation buildCachedInvocation(ProceedingJoinPoint proceedingJoinPoint,CustomizedCacheable customizedCacheable){
        Method method = this.getSpecificmethod(proceedingJoinPoint);
        String[] cacheNames = customizedCacheable.cacheNames();
        Object targetBean = proceedingJoinPoint.getTarget();
        Object[] arguments = proceedingJoinPoint.getArgs();
        Object key = customizedCacheable.key();
        CachedInvocation cachedInvocation = CachedInvocation.builder()
                .arguments(arguments)
                .targetBean(targetBean)
                .targetMethod(method)
                .cacheNames(cacheNames)
                .key(key)
                .expiredTimeSecond(customizedCacheable.expiredTimeSecond())
                .preLoadTimeSecond(customizedCacheable.preLoadTimeSecond())
                .build();
        return cachedInvocation;
    }

    private Method getSpecificmethod(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        // The method may be on an interface, but we need attributes from the
        // target class. If the target class is null, the method will be
        // unchanged.
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(pjp.getTarget());
        if (targetClass == null && pjp.getTarget() != null) {
            targetClass = pjp.getTarget().getClass();
        }
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        // If we are dealing with method with generic parameters, find the
        // original method.
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
        return specificMethod;
    }

    private String parseSPELKey(CustomizedCacheable cacheable, StandardEvaluationContext context) {
        String keySpel = cacheable.key();
        Expression expression = new SpelExpressionParser().parseExpression(keySpel);
        String key = expression.getValue(context, String.class);
        return key;
    }

}

拦截器利用了AOP思想,达到对业务代码的无侵入性。通过mvn install下载到本地maven仓库或mvn deploy部署到远程仓库。这样其他项目在使用该组件时,只需要在pom中引入该依赖包,然后在方法上加上自定义注解即可。

@PostMapping("/2")
@CustomizedCacheable(value = "22", key = "11", cacheType = "2", expiredTimeSecond = 100, preLoadTimeSecond = 40)
public String test2() {
    //...
}

完整项目地址:https://github.com/xiaoyir/multiTtlCache

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值