基于SpringAOP写个简单的Redis缓存

缓存应用场景非常多,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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值