缓存应用场景非常多,Spring框架中对缓存的抽象与支持已经非常全面。有时候本地缓存是不够的,需要分布式缓存,本文尝试基于Spring+Redis实现一个简单的分布式缓存。
spring-data-redis中已经有基于Redis的缓存实现,感兴趣的小伙伴可自行研究。网上也有一些优秀的开源方案,如阿里的jetcache可供参考。大部分方案在redis序列化时采用的是JDK序列化方式,这种方式的问题也很明显,需要显式支持Serializable接口,以及性能、兼容性(serialVersionUID变更)、直观性等问题。个人更倾向于json方式,可json在反序列化遇到泛型擦除问题也是一大麻烦,Jackson、fastjson、GSON等各大框架都有自己的解决办法。
废话不多说,参考spring缓存操作注解,我们也先定义缓存操作(读、强写、清除)的几个注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCacheable {
/**
* redisTemplate的bean名称
*/
String redisBean() default "";
/**
* 过期时间(秒),默认60秒
*/
long expire() default 60;
/**
* 缓存名称
*/
String name() default "";
/**
* 缓存key,SpEL表达式
*/
String key() default "";
/**
* 是否使用gzip压缩
*/
boolean gzip() default false;
/**
* 是否缓存null数据
*/
boolean cacheNull() default false;
/**
* 同步加载缓存数据
*/
boolean syncLoad() default false;
/**
* 缓存时间策略实现类
*/
Class<? extends ExpireStrategy> expireStrategy() default ExpireStrategy.class;
/**
* 本地缓存名,非空时启用本地缓存
*/
String localName() default "";
/**
* 本地缓存管理器,非空时使用指定的缓存管理器
*/
String localCacheManager() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCachePut {
/**
* redisTemplate的bean名称
*/
String redisBean() default "";
/**
* 过期时间(秒),默认60秒
*/
long expire() default 60;
/**
* 缓存名称
*/
String name() default "";
/**
* 缓存key,SpEL表达式
*/
String key() default "";
/**
* 是否使用gzip压缩
*/
boolean gzip() default false;
/**
* 是否缓存null数据
*/
boolean cacheNull() default false;
/**
* 同步加载缓存数据
*/
boolean syncLoad() default false;
/**
* 缓存时间策略实现类
*/
Class<? extends ExpireStrategy> expireStrategy() default ExpireStrategy.class;
/**
* 本地缓存名,非空时启用本地缓存
*/
String localName() default "";
/**
* 本地缓存管理器,非空时使用指定的缓存管理器
*/
String localCacheManager() default "";
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCacheEvict {
/**
* redisTemplate的bean名称
*/
String redisBean() default "";
/**
* 缓存名称
*/
String name() default "";
/**
* 缓存key,SpEL表达式
*/
String key() default "";
/**
* 本地缓存名,非空时启用本地缓存
*/
String localName() default "";
/**
* 本地缓存管理器,非空时使用指定的缓存管理器
*/
String localCacheManager() default "";
}
可以看到我们的目标是:
1、支持两级缓存:Redis分布式缓存+本地缓存;
2、动态配置缓存时间;
3、支持序列化后的值gzip压缩;
为什么需要动态缓存时间呢?如不同环境下缓存时间有不同需求、数据内容不同时缓存时间需求也不同等等。
为什么要支持gzip压缩呢?缓存一些大对象时,有压缩跟没压缩对网络传输的时间是明显不同的,gzip是压缩算法里综合性能较好的,简单易用。
定义好了注解,在Spring框架下定义对注解的AOP处理即可使用了。实现代码也是参考Spring缓存实现,代码比较长,具体可参考github。
下面是划重点:
1、同步加载(避免缓存击穿),使用InvokeUtils,借助CountDownLatch实现同步;
2、智能反序列化AutoJsonRedisSerializer。这个比较有意思,即List<Map<String,List>>这样的类型如何反序列化出来,各大json框架都有TypeReference这样类似的机制,一开始是采用动态编译生成TypeReference对象取得Type进行反序列化,这里涉及的是动态编译技术,也是一大门学问,直接copy了网上大牛的代码。后来发现guava里直接有更简单的方案,即TypeToken,不得不感叹自己是坐井观天。真是学海无涯、学无止境!
那么最终配置好redisTemplate以及本地缓存(可选)、配置下AOP,即可使用注解方式启用这个二级分布式缓存功能。参考如下:
<bean id="commonRedisCacheConfig" class="com.alpha.coding.common.redis.cache">
<property name="redisTemplate" ref="stringRedisTemplate"/>
<property name="name" value="RDS:CH"/>
<property name="expire" value="600"/>
</bean>
<bean id="annotationRedisCacheAspect" class="com.alpha.coding.common.redis.cache.RedisCacheAspect">
<property name="cacheConfig" ref="commonRedisCacheConfig"/>
<property name="localCacheManager" ref="caffeineCacheManager"/>
</bean>
<aop:config proxy-target-class="true">
<!-- 基于注解的RedisCache -->
<aop:aspect ref="annotationRedisCacheAspect" order="1">
<aop:around method="doCacheAspect"
pointcut="@within(com.alpha.coding.common.redis.cache.annotation.RedisCacheable)
|| @annotation(com.alpha.coding.common.redis.cache.annotation.RedisCacheable)
|| @within(com.alpha.coding.common.redis.cache.annotation.RedisCachePut)
|| @annotation(com.alpha.coding.common.redis.cache.annotation.RedisCachePut)
|| @within(com.alpha.coding.common.redis.cache.annotation.RedisCacheEvict)
|| @annotation(com.alpha.coding.common.redis.cache.annotation.RedisCacheEvict)"/>
</aop:aspect>
</aop:config>
使用示例如下:
@Component
public class SomeServiceWithCache implements ExpireStrategy {
@RedisCacheable(key = "'redis_cache_example'", syncLoad = true, expireStrategy = SomeServiceWithCache.class)
public List<Map<String, List<Integer>>> doSomeWithCache() {
// TODO load data
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Arrays.asList(Collections.singletonMap("test", Arrays.asList(1)));
}
@Override
public long calculateExpire(Object[] args, Object returnValue) {
return returnValue == null ? 5 : 60;
}
}