Spring-Cache扩展
SPRING-CHACE 微服务协作
由于出色的易用性,Spring-Cache 在各种项目中使用非常广泛,并且各种缓存组件也有这良好的支持,从老牌的Ehcache到Redis,本达到开箱即用的程度。只需要简单的几个注解就可以开启缓存来提升性能。
本文并不介绍SpringCache的使用,主要记录在微服务环境下遇到的实际问题,与一种解决思路
背景
SHADOW最近的在做的项目由于各种配置类的数据较多,并且可能需要跨库查询, 显然从数据库查询显然不是很好的方式。并且多库多表代码量多且重复度高。
考虑到性能与开发的效率,而且数据的变更频率其实不高,预估1小时的同步周期就可以满足业务需求,所以打算把配置类数据和部分业务数据整合抽取到Redis。
很显然,Spring-Cache是一个非常不错的选择,业务模块通过@Cacheable
从Redis中获取数据,数据模块同样通过@Cacheable
定时将数据同步到Redis。各个模块各区所需。
大致示意图如下:
问题
想法很好,写了两个例子开始测试,把部分数据同步到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是通过切面实现的缓存,其实可以拿到返回泛型
所以需要解决的问题两个
- 自定义Redis序列化
- 解决实现类无法获取到希望的返回类型的问题
解决方案
第一个比较好解决,实现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);
}
}
}
或者也可以把映射类抽取到共用模块中,做为基础依赖包,这样做在内部项目中没有问题。
但是现在很多情况都是多项目,多合作方联合开发,这部分数据就不太好用,想要使用就只有
- 不使用
@Cacheable
,单独获取redis数据。 - 引入基础依赖包,对于外部项目或者合作方有一定的侵入性,不够友好。
后记
一个小问题,代码量也不多,做个记录希望帮到有需要的人。