前文

在完成了前面两篇文章(基于AOP的Redis缓存注解功能开发设计[2],JexlEngine自定义缓存Key)中的基础功能的开发之后,我们可以再进一步完成缓存更新、缓存删除、缓存过期这几个接口。从而适应在具体的业务开发过程中遇到的各种对缓存数据操作的需求。

因为前面对具体的Redis的缓存的操作和实现逻辑已经做了详细说明,所以在这三个功能注解的开发过程中只做简单的逻辑阐述,不在细究每一个方法的具体含义

缓存更新

缓存更新的意思就是,当我们在更新数据库中的某条数据之后,我们需要另外也把Redis中作为缓存的数据也同步时更新下,这里其实就会涉及到我们之前提到的缓存一致性问题,这个话题要讲的话可以展开来讲很长一段,我们不在这里做过多讨论,可以自行再去搜索一番。

我在这里需要提供一个新的注解@RedisPut,和PointCut来实现我们的功能即可,代码直接加到原来的切面类中。切面类中需要实现的逻辑就是根据方法执行的结果,直接更新Redis中的缓存数据,和之前@RedisCache的区别就是这里不需要去Redis中判断是否已经存在缓存数据。

具体何时调用这个注解所在的方法,由具体业务的开发人员自行根据业务需求和缓存一致性的精准度要求自行实现。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisPut {
    String value();

    String key() default "";
}
@Pointcut(value = "@annotation(com.cheungq.demo.cache.annotation.RedisPut)")
public void pointcutRedisPut(){}

@SneakyThrows
@Around(value = "pointcutRedisPut()")
public Object redisPutAround(ProceedingJoinPoint joinPoint){
    MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
    Method method = joinPointObject.getMethod();
    RedisPut cacheAnnotation = method.getAnnotation(RedisPut.class);
    String key = cacheAnnotation.value();
    Map<String, Object> paramMap = paramMap(joinPointObject.getMethod().getParameters(), joinPoint.getArgs());
    String jexlExp = cacheAnnotation.key();
    String hKey = (String)generateHashKey(jexlExp,paramMap);
    logger.info("new CACHE to " +key+" hashKey: "+ hKey);
    Object res = joinPoint.proceed();
    cacheToRedis(key,hKey,res);
    return res;
}

缓存删除

同样的,完成了前面的缓存更新之后,对于缓存删除的逻辑我们也可以类似的操作,写缓存的时候我们调用的是stringRedisTemplate.opsForHash().put()方法,删除的话我们也只需要调用下stringRedisTemplate.opsForHash().delete()即可。

除了删除hash表中的缓存数据,如果没有指定hash表中的hashKey的话,我们也可以默认删除整个Redis的key,并按照这个思路对代码进行一些调整

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisRemove {
    String value();

    String key() default "";
}
@Pointcut(value = "@annotation(com.cheungq.demo.cache.annotation.RedisRemove)")
public void pointcutRedisRemove(){}

@SneakyThrows
@Around(value = "pointcutRedisRemove()")
public Object redisRemoveAround(ProceedingJoinPoint joinPoint) {
    MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
    Method method = joinPointObject.getMethod();
    RedisRemove cacheAnnotation = method.getAnnotation(RedisRemove.class);
    String key = cacheAnnotation.value();
    String jexlExp = cacheAnnotation.key();
    if (StringUtils.isBlank(jexlExp)) {
        logger.info("remove CACHE " + key);
        deleteByRedisKey(key);
    } else {
        Map<String, Object> paramMap = paramMap(joinPointObject.getMethod().getParameters(), joinPoint.getArgs());
        String hKey = (String) generateHashKey(jexlExp, paramMap);
        logger.info("remove CACHE " + key + " hashKey: " + hKey);
        deleteFromRedisHash(key, hKey);
    }
    return joinPoint.proceed();
}

private void deleteFromRedisHash(String key, Object hashKey){
    try {
        stringRedisTemplate.opsForHash().delete(
                key,
                new Object[]{SerializeUtil.serialize(hashKey)}
        );
    }catch (Exception e){
        logger.error("删除Redis异常",e);
    }
}
private void deleteByRedisKey(String key){
    try {
        stringRedisTemplate.delete(key);
    }catch (Exception e){
        logger.error("删除Redis异常",e);
    }
}

缓存过期

缓存过期只能针对于整个RedisKey,所以我们不再需要原来的key变量,但是因为过期需要一个过期时长的参数,所以我们需要调整下,给予一个默认10分钟缓存过期的参数

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisExpire {
    String value();

    int seconds() default 600;
}

切面类中的代码部分则做类似的操作

@Pointcut(value = "@annotation(com.cheungq.demo.cache.annotation.RedisExpire)")
public void pointcutRedisExpire(){}

@SneakyThrows
@Around(value = "pointcutRedisExpire()")
public Object redisExpireAround(ProceedingJoinPoint joinPoint) {
    MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
    Method method = joinPointObject.getMethod();
    RedisExpire cacheAnnotation = method.getAnnotation(RedisExpire.class);
    String key = cacheAnnotation.value();
    int seconds = cacheAnnotation.seconds();
    logger.info("expire CACHE " + key + " time: " + seconds + "s.");
    expireRedis(key, seconds);
    return joinPoint.proceed();
}


private void expireRedis(String key, int seconds){
    try {
        stringRedisTemplate.expire(
                key,
                seconds,
                TimeUnit.SECONDS
        );
    }catch (Exception e){
        logger.error("写入Redis异常",e);
    }
}

自己写几个demo都跑一下,测试确认没有问题,那么这个功能就基本暂时算是基本完备了。

结语

到这里,我们自己开发的基于AOP的Redis缓存注解的功能就算是基本都开发完成了。其中涉及到了各种知识,包括但不限于AOP的实现、RedisClient的配置管理、Redis的Hash结构的学习,Java对象的序列化与反序列化以及JexlEngine表达式引擎的应用等,当然还有我在文章中没有详细写出但是提到的Mysq与Redis的缓存一致性问题的思考。

1 基于AOP的Redis缓存注解功能开发设计

2 基于AOP的Redis缓存注解功能开发设计[2],JexlEngine自定义缓存Key

3 基于AOP的Redis缓存注解功能开发设计[3],缓存更新、删除、过期

4 Java对象序列化常用操作-Protostuff

5 使用Objenesis实例化java对象

6 JexlEngine表达式引擎的简单使用