李解的博客

2021-08-12

Spring-Cache扩展

SPRING-CHACE 微服务协作

由于出色的易用性,Spring-Cache 在各种项目中使用非常广泛,并且各种缓存组件也有这良好的支持,从老牌的Ehcache到Redis,本达到开箱即用的程度。只需要简单的几个注解就可以开启缓存来提升性能。

image-20210318221358866

本文并不介绍SpringCache的使用,主要记录在微服务环境下遇到的实际问题,与一种解决思路

背景

SHADOW最近的在做的项目由于各种配置类的数据较多,并且可能需要跨库查询, 显然从数据库查询显然不是很好的方式。并且多库多表代码量多且重复度高。

考虑到性能与开发的效率,而且数据的变更频率其实不高,预估1小时的同步周期就可以满足业务需求,所以打算把配置类数据和部分业务数据整合抽取到Redis。

很显然,Spring-Cache是一个非常不错的选择,业务模块通过@Cacheable 从Redis中获取数据,数据模块同样通过@Cacheable定时将数据同步到Redis。各个模块各区所需。

大致示意图如下:

image-20210318223508261

问题

想法很好,写了两个例子开始测试,把部分数据同步到redis,然后应用再写一个缓存接口,方法打上@Cacheable,例如同步和模块都是

// 数据模块Bean
package com.shadow.data;
class Ticket {
  private String field1;
  ...
  private String field100;
}
// 业务模块bean,只取自己需要的
package com.shadow.biz;
class Ticket {
  private String field1;
  private String field3;
  private String field48;
}
// 缓存
@Cacheable(cacheNames = "LESS-CHANGE-CACHE", key = "'all-ticket'")
public List<Ticket> allTicket();

建议同一类型缓存都放在同一个cacheName下,方便管理与查看

齐活,模块启动,数据开始同步,通过测试类获取,然后....报错,提示无法实例化com.shadow.data.Ticket这个类

难道是使用默认的binary序列化的原因么?换成jaskson试试?结果也是一样,打开工具看一下redis,懵逼了,redis实际存储的和想象的不一样

[
	{
		"@class": "com.shadow.data.Ticket",
		"field1": "",
		...
	}
]

这是为什么呢?为什么多了一个@class的属性

排查

通过源码发现,Redis序列化需要实现RedisSerializer, 核心源码如下:

@Nullable
byte[] serialize(@Nullable T t) throws SerializationException;

@Nullable
T deserialize(@Nullable byte[] bytes) throws SerializationException;

所以在序列化与反序列化时,实际上并没有办法通过泛型来做映射。

这块存疑,可能有误,因为理论上是可行的,@Cacheable是通过切面实现的缓存,其实可以拿到返回泛型

所以需要解决的问题两个

  1. 自定义Redis序列化
  2. 解决实现类无法获取到希望的返回类型的问题

解决方案

第一个比较好解决,实现RedisSerilizer通过自有的Json工具实现对象与json的转换

第二个比较头疼,因为如果在方法内通过堆栈获取调用者的信息,其实是拿不到原始调用者的返回类型的,因为@Cacheable本身就是切面实现的,可以通过多次向上反推,但是很明显不合理。

这里想的是通过AOP切入@Cacheable 获取实际的原始返回类型,然后通过ThreadLocal传递,这样实现类就可以拿到真实的返回类型。

SHADOW比较懒,直接用的hutool。代码如下:

/**
 * 缓存切面
 * <p>
 * 环切 {@link org.springframework.cache.annotation.Cacheable}
 * 通过ThreadLocal记录返回类型
 * 解决序列化无法映射到类的问题
 *
 * @author shadow
 * @version 1.3
 * @date 2021-03-16 14:45:21
 * @since 1.3
 */
@Component
@Aspect
@Order(Integer.MIN_VALUE)
@ConditionalOnClass(value = {RedisCacheConfiguration.class})
public class CacheAspect {

    /**
     * 环绕
     * 执行前后对{@link CacheContext} 进行设置
     *
     * @param joinPoint 切面参数
     * @return 方法返回对象
     * @throws Throwable 异常
     */
    @Around("@annotation(org.springframework.cache.annotation.Cacheable)")
    public Object cacheCut(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取原始类型
        Type type = signature.getMethod().getGenericReturnType();
        // 获取返回类型映射
        CacheContext.set(type);
        Object returnValue = joinPoint.proceed();
        // 执行后清空
        CacheContext.clear();
        return returnValue;
    }
}

/**
 * 缓存上下文
 * <p>
 * 解决序列化无法映射到类的问题
 *
 * @author shadow
 * @version 1.0
 * @date 2021-03-16 14:50:48
 * @since 1.0
 */
public class CacheContext {

    /**
     * 构造私有
     */
    private CacheContext() {

    }

    /**
     * 线程变量
     */
    private static final ThreadLocal<Type> TYPE_CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置类
     */
    public static void set(Type type) {
        TYPE_CONTEXT_HOLDER.set(type);
    }

    /**
     * 获取类
     *
     * @return {@link #TYPE_CONTEXT_HOLDER}
     */
    public static Type get() {
        return TYPE_CONTEXT_HOLDER.get();
    }

    /**
     * 清除变量
     */
    public static void clear() {
        TYPE_CONTEXT_HOLDER.remove();
    }
}

/**
 * 缓存配置
 * <p>
 * 自动配置条件为检测到{@link RedisCacheConfiguration}类
 * 自动开启缓存
 *
 * @author shadow
 * @version 1.0
 * @date 2020-09-26
 * @since 1.0
 */
@Configuration
@ConditionalOnClass(value = {RedisCacheConfiguration.class})
@EnableCaching
public class CacheAutoConfiguration {

    /** 日志 */
    private static final Logger LOGGER = LoggerFactory.getLogger(CacheAutoConfiguration.class);
    /** 缓存配置 */
    @Resource
    private CacheProperties cacheProperties;

    /**
     * 配置Redis缓存配置,序列化json
     * <p>
     * 简单配置,需要再增加参数
     * 
     *
     * @return {@link RedisCacheConfiguration }
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        // json序列化
        GenericJsonRedisSerializer genericJsonRedisSerializer = new GenericJsonRedisSerializer();

        // redisCache配置
        CacheProperties.Redis redisCacheProperties = cacheProperties.getRedis();
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
        if (!redisCacheProperties.isCacheNullValues()) {
            defaultCacheConfig = defaultCacheConfig.disableCachingNullValues();
        }
        return defaultCacheConfig.entryTtl(redisCacheProperties.getTimeToLive())
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJsonRedisSerializer));

    }

    /**
     * 自定义JsonRedis序列化
     */
    class GenericJsonRedisSerializer implements RedisSerializer {

        /**
         * 反序列化
         *
         * @param bytes 字节
         * @return 对象 Class<?> clazz
         * @throws SerializationException e
         */
        @Override
        public Object deserialize(byte[] bytes) throws SerializationException {
            Type returnType = CacheContext.get();
            if (returnType == null) {
                LOGGER.error("<= 无法获取反射类,请确认是否可从切面(CacheAspect)获取");
            }
            String strValue = StrUtil.str(bytes, StandardCharsets.UTF_8);
            return JSONUtil.toBean(strValue, returnType, true);
        }

        /**
         * 序列化
         *
         * @param target 对象
         * @return byte[]
         * @throws SerializationException e
         */
        @Override
        public byte[] serialize(Object target) throws SerializationException {
            return JSONUtil.toJsonStr(target).getBytes(StandardCharsets.UTF_8);
        }
    }

}

或者也可以把映射类抽取到共用模块中,做为基础依赖包,这样做在内部项目中没有问题。

但是现在很多情况都是多项目,多合作方联合开发,这部分数据就不太好用,想要使用就只有

  1. 不使用@Cacheable,单独获取redis数据。
  2. 引入基础依赖包,对于外部项目或者合作方有一定的侵入性,不够友好。

后记

一个小问题,代码量也不多,做个记录希望帮到有需要的人。