Redis最佳实践,再也不用手动写缓存代码了

1,008 阅读4分钟

背景

在项目中我们常常需要使用Redis或者其他缓存方案,在项目中很多使用缓存的代码都是"八股文",实现缓存逻辑是一致的,但项目组每个人实现缓存的代码又很难保证一致,业务逻辑与缓存代码耦合,项目代码可读性下降。

缓存框架优点

  1. 实现业务逻辑与缓存代码解耦,保证业务代码"干净"。
  2. 提升开发效率,开发过程中可以更专注业务代码。
  3. 提升代码可读性。
  4. 避免重复实现缓存代码,减少人工实现缓存代码产生的BUG。

缓存框架实现思路

缓存伪代码

查询缓存伪代码

public Object testQueryPseudocode() {
    // 1.查缓存
    String cacheValue = getRedisValue(redisKey);
    // 2.redis的值存在则反序列化并返回
    if (cacheValue != null&&cacheValue.length()>0) {
        return JSON.parseObject(cacheValue, Object.class);
    }
    // 3.从DB中获取数据
    Object result = getDBValue();
    // 4.将结果保存到Redis中
    setRedisValue(redisKey, result);
    return result;
}

删除缓存伪代码

public void testDeletePseudocode() {
    // 1. 删除缓存
    deleteRedisValue(redisKey);
    // 2. 更新或删除业务逻辑
    deleteDBValue();
}

思路分析

查询缓存伪代码中,这些步骤的顺序都不会改变,1、2、4步都属于缓存代码,这部分代码不会改变,3是业务逻辑代码。可以通过AOP中的环绕通知实现这部分代码。

删除缓存伪代码中,只需要在业务逻辑之前删除缓存,可以通过AOP的前置通知实现这部分代码。

AOP方式实现的伪代码

查询方法

在查询接口上需要在业务逻辑前查缓存,业务逻辑后设置缓存。由于传参不通,构建出缓存的key也不相同,我们可以通过注解@RedisGetKey判断接口是否要添加缓存,@RedisGetKey中的keyprefix@RedisParam共同构建出redis的Key。

@RedisGetKey(keyPrefix = USER_REDIS_KEY_PREFIX)
public User getUserById(@RedisParam Long id) {
    // todo 业务代码
}

更新/删除方法

在更新/删除方法前通常需要删除缓存中的数据,尽量保证缓存和DB中数据一致。可以通过@RedisDeleteKey判断方法是否要删除缓存,@RedisDeleteKey中的keyprefix@RedisParam共同构建出redis的Key。

@RedisDeleteKey(keyPrefix = USER_REDIS_KEY_PREFIX)
public void deleteUser(@RedisParam Long id) {
    // todo 业务代码
}

代码实现

1. 写缓存注解

查缓存的注解,可以标注在方法上

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisGetKey {
    /**
     * RedisKey前缀
     *
     * @return
     */
    String keyPrefix();

    /**
     * 缓存时间,单位是s
     *
     * @return
     */
    long cachePeriod() default 7200L;

    /**
     * 方法是否需要缓存参数
     *
     * @return
     */
    boolean paramNeed() default true;
}

删除缓存的注解,可以标注在方法上

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisDeleteKey {
    /**
     * RedisKey前缀
     *
     * @return
     */
    String keyPrefix();

    /**
     * 是否需要缓存参数
     *
     * @return
     */
    boolean paramNeed() default true;

}

缓存参数注解,可以标注在参数上

@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisParam {
}

2. 缓存切面

首先写一个根据方法签名和参数构建的缓存key的父类

public abstract class RedisAspect {
    public String getParameterStr(MethodSignature methodSignature, Object[] args) {
        StringBuilder paramStringBuilder = new StringBuilder();
        Method method = methodSignature.getMethod();
        Parameter[] parameters = method.getParameters();
        // 1. 遍历参数,找出包含@RedisParam注解的参数,并且构建参数key
        if (parameters != null && parameters.length > 0) {
            for (int i = 0; i < parameters.length; i++) {
                RedisParam redisParam = parameters[i].getDeclaredAnnotation(RedisParam.class);
                if (redisParam != null) {
                    if (paramStringBuilder.length() != 0) {
                        paramStringBuilder.append(":");
                    }
                    paramStringBuilder.append(args[i]);
                }
            }

        }
        String paramStr = paramStringBuilder.toString();
        if (StringUtils.isEmpty(paramStr)) {
            throw new RuntimeException("Redis param can't be empty!");
        }
        return paramStr;
    }
}

删除Redis缓存切面

@Aspect
@Component
@Slf4j
public class RedisDeleteAspect extends RedisAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Pointcut(value = "@annotation(com.linqi.webtest.redis.annotation.RedisDeleteKey)")
    public void redisPointCut() {
    }

    @Before("redisPointCut() && @annotation(redisDeleteKey)")
    public void doDeleteRedisKey(JoinPoint joinPoint, RedisDeleteKey redisDeleteKey) throws Throwable {
        String paramStr = null;
        // 1. 构建缓存key参数
        if (redisDeleteKey.paramNeed()) {
            paramStr = getParameterStr((MethodSignature) joinPoint.getSignature(), joinPoint.getArgs());
        }
        String redisFullKey = redisDeleteKey.keyPrefix() + paramStr;
		// 2. 删除缓存
        Boolean deleteResult = stringRedisTemplate.delete(redisFullKey);
        log.info("delete redis key! redisKey={}, result={}", redisFullKey, deleteResult);
    }
}

获取Redis缓存数据切面

@Aspect
@Component
@Slf4j
public class RedisGetAspect extends RedisAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Pointcut(value = "@annotation(com.linqi.webtest.redis.annotation.RedisGetKey)")
    public void redisPointCut() {
    }

    @Around("redisPointCut() && @annotation(redisGetKey)")
    public Object doRedisGet(ProceedingJoinPoint proceedingJoinPoint, RedisGetKey redisGetKey) throws Throwable {
        String paramStr = null;
        Object[] args = proceedingJoinPoint.getArgs();
        // 1. 构建缓存参数
        if (redisGetKey.paramNeed()) {
            if (args == null || args.length == 0) {
                throw new IllegalArgumentException();
            }
            paramStr = getParameterStr((MethodSignature) proceedingJoinPoint.getSignature(), args);
        }
        String redisFullKey = redisGetKey.keyPrefix() + paramStr;
        Type type = getReturnType(proceedingJoinPoint);
        // 2. 获取缓存数据,如果缓存有数据,则反序列化后直接返回
        String value = stringRedisTemplate.opsForValue().get(redisFullKey);

        Object result = null;
        if (!StringUtils.isEmpty(value)) {
            log.info("success get result from redis string ! redisKey={}", redisFullKey);
            Class<?> returnTypeClass = ((MethodSignature) proceedingJoinPoint.getSignature()).getReturnType();
            if (type instanceof List) {
                return JSON.parseArray(value, returnTypeClass);
            } else {
                return JSON.parseObject(value, returnTypeClass);
            }
        } else {
            // 3. 执行代理方法,获取数据
            result = proceedingJoinPoint.proceed(args);
            String resultStr = JSON.toJSONString(result);
            // 4. 将结果设置到缓存中
            stringRedisTemplate.opsForValue().set(redisFullKey, resultStr, redisGetKey.cachePeriod());
            log.info("success set redis string! key={}",redisFullKey);
            return result;
        }

    }

    private Type getReturnType(ProceedingJoinPoint proceedingJoinPoint) throws NoSuchMethodException {
        //获取方法
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        //获取返回值类型
        return method.getAnnotatedReturnType().getType();
    }
}

缓存框架测试

测试代码

请勿吐槽测试用的代码!!!请勿吐槽测试用的代码!!!请勿吐槽测试用的代码!!!都是瞎写。。。

  1. 测试用的方法,一个是查缓存,一个是删缓存
@Slf4j
@Service
public class RedisTestServiceImpl implements RedisTestService {

    private static final String USER_REDIS_KEY_PREFIX = "sales:user:";

    private static final String USER_ID_USER_NAME_REDIS_KEY_PREFIX = "user:id:name:";

    /**
     * 根据用户id获取用户
     **/
    @Override
    @RedisGetKey(keyPrefix = USER_REDIS_KEY_PREFIX)
    public User getUserById(@RedisParam Long id) {
        if (id == null) {
            return null;
        }

        if (id.equals(1L)) {
            return User.builder().id(id).name("haha").build();
        } else {
            return User.builder().id(id).name("world" + id).build();
        }
    }

    /**
     * 根据用户id删除用户
     **/
    @Override
    @RedisDeleteKey(keyPrefix = USER_REDIS_KEY_PREFIX)
    public void deleteUser(@RedisParam Long id) {
        log.info("success delete user!");
    }
}
  1. 编写单元测试
@Test
public void getUserById() throws InterruptedException {
    for (int i = 0; i < 30; i++) {
        User user1 = redisTestService.getUserById(1L);
        log.info("get user1!user1={}",user1);
        User user2 = redisTestService.getUserById(2L);
        log.info("get user2!user2={}",user2);

        if (i % 2 == 0) {
            redisTestService.deleteUser(1L);
        }
        if (i % 3 == 0) {
            redisTestService.deleteUser(2L);
        }
        Thread.sleep(1000);
    }
}

测试结果