AOP 织入 Redis 缓存

 

  场景:页面包含多个大 sql。
  目的:尽量保证接口响应速度,数据库压力可暂不考虑(并发不大,耗时 sql 多)。
  思路:

  1、如果 redis 中不存在缓存,查询数据库并添加缓存,根据数据变化频率设置缓存过期时间;

  2、如果 redis 中存在缓存,提交更新缓存的异步任务(可选,针对数据变化频率高,但业务上不是特别敏感的情况),返回缓存;

  3、对于数据变化较频繁的接口,使用定时任务定时更新 redis 中的缓存(数据变化频率高,且业务敏感)。

  对于查询数据库并更新缓存的任务,需要使用分布式锁进行调度。防止多个线程同时做相同的操作浪费系统资源(毕竟是大 sql,要尽量避免不必要的请求穿透到数据库)。

  首先实现分布式锁,通过 SETNX 结合 LUA 实现:

public interface RedisLockUtil {
    //加锁
    public boolean lock(String key, String requestId) throws Exception;

    //解锁
    public boolean unlock(String key, String requestId) throws Exception;
}

  实现:

public class ProtoRedisLockUtil implements RedisLockUtil {

    public ProtoRedisLockUtil(StringRedisTemplate redisTemplate, int cacheTime) {
        this.redisTemplate = redisTemplate;
        this.cacheTime = cacheTime;
    }

    private StringRedisTemplate redisTemplate;
    //缓存存活时间
    private int cacheTime;
    //Key 的前缀
    private String preText = "RedisLock|";

    /**
     * @Author
     * @Date 2021/9/27 上午12:11
     * @Description 缓存格式:过期时间的时间戳|请求唯一标识
     * 通过 SETNX 模拟 getAndSet
     * 通过 LUA 脚本保证 "删除过期锁、上锁" 这一对操作的原子性
     */
    @Override
    public boolean lock(String key, String requestId) throws InterruptedException {
        key = preText + key;
        int tryCount = 3;
        while (tryCount > 0) {
            long currentTime = System.currentTimeMillis();
            //缓存存活的最终时间
            Long overdueTime = currentTime + this.cacheTime;
            String val = overdueTime + "|" + requestId;
            //竞争到锁
            if (redisTemplate.opsForValue().setIfAbsent(key, val)) {
                System.out.println("竞争锁成功!");
                return true;
            }
            //LUA 脚本,删除成功返回 1 ,失败返回 0;SET 成功返回 OK;GET 成功返回值,GET 失败返回 null
            StringBuilder USER_AIMS_GOLD_LUA = new StringBuilder();
            USER_AIMS_GOLD_LUA.append("local value = redis.call('get',KEYS[1]);");
            USER_AIMS_GOLD_LUA.append("if not value then return '-1'; end;");
            USER_AIMS_GOLD_LUA.append("local position = string.find(value,'|');");
            USER_AIMS_GOLD_LUA.append("local timeStemp = string.sub(value,0,position-1)");
            USER_AIMS_GOLD_LUA.append("timeStemp = tonumber(timeStemp)");
            USER_AIMS_GOLD_LUA.append("local currentTime = tonumber(ARGV[1])");
            USER_AIMS_GOLD_LUA.append("if currentTime>timeStemp then redis.call('del',KEYS[1]);");
            USER_AIMS_GOLD_LUA.append("if redis.call('setnx', KEYS[1], ARGV[2])==1 then return '1'; " +
                    "else return '0';end;");
            USER_AIMS_GOLD_LUA.append("else return '0';end;");
            DefaultRedisScript defaultRedisScript = new DefaultRedisScript();
            defaultRedisScript.setScriptText(USER_AIMS_GOLD_LUA.toString());
            defaultRedisScript.setResultType(String.class);
            List<String> keyList = new ArrayList();
            keyList.add(key);
            Object result = redisTemplate.execute(defaultRedisScript, keyList, String.valueOf(currentTime),
                    val);
            //删除过期锁并竞争锁成功
            if ("1".equals(result.toString())) {
                System.out.println("删除过期锁并竞争锁成功!");
                return true;
            }
            //未竞争到锁,检查当前锁是否已到期。防止死锁
            tryCount--;
            Thread.sleep(200);
        }
        System.out.println("竞争锁失败!");
        return false;
    }

    /**
     * @Author
     * @Date 2021/9/26 下午10:48
     * @Description 释放锁
     * 通过 LUA 脚本保证 "核对 uuid 、释放锁" 这一对动作的原子性
     */
    @Override
    public boolean unlock(String key, String requestId) {
        key = preText + key;
        StringBuilder USER_AIMS_GOLD_LUA = new StringBuilder();
        USER_AIMS_GOLD_LUA.append("local value = redis.call('get',KEYS[1]);");
        USER_AIMS_GOLD_LUA.append("if not value then return '-1'; end;");
        USER_AIMS_GOLD_LUA.append("local position = string.find(value,'|');");
        USER_AIMS_GOLD_LUA.append("local requestId = string.sub(value,position+1)");
        USER_AIMS_GOLD_LUA.append("if ARGV[1]==requestId then ");
        USER_AIMS_GOLD_LUA.append("redis.call('del',KEYS[1]);return '1';");
        USER_AIMS_GOLD_LUA.append("else return '0'; end;");
        DefaultRedisScript defaultRedisScript = new DefaultRedisScript();
        defaultRedisScript.setScriptText(USER_AIMS_GOLD_LUA.toString());
        defaultRedisScript.setResultType(String.class);
        List<String> keyList = new ArrayList();
        keyList.add(key);
        Object result = redisTemplate.execute(defaultRedisScript, keyList, requestId);
        if ("1".equals(result)) System.out.println("自行释放锁成功");
        return "1".equals(result);
    }
}

  缓存相关操作通过 AOP 织入业务代码,切面实现:

@Aspect
@Component
public class RedisCacheAspect {
    private static final Logger logger = Logger.getLogger(RedisCacheAspect.class);
    //分隔符
    private static final String DELIMITER = "|";
    //默认缓存存活时间,单位:分钟
    private static final int defaultCacheTime = 60;

    @Resource
    RedisTemplate<String, Object> redisTemplate;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    //异步任务池
    @Autowired
    private ThreadPoolExecutor poolExecutor;

    //切点
    @Pointcut("@annotation(com.inspur.redisCache.annotation.RedisCache)")
    public void redisCacheAspect() {
    }

    //切面
    @Around("redisCacheAspect()")
    public Object cache(ProceedingJoinPoint joinPoint) {
        String clazzName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String key = this.getRedisKey(clazzName, methodName, args);
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //注解配置
        int cacheTimeMin = method.getAnnotation(RedisCache.class).cacheTime();
        boolean refreshCacheWithSelect = method.getAnnotation(RedisCache.class).refreshWithSelect();
        Object cacheValue = redisTemplate.boundValueOps(key).get();
        //缓存命中
        if (cacheValue != null) {
            logger.info("Redis 缓存命中:" + key);
            // 若缓存命中时也需更新缓存,提交异步任务
            if (refreshCacheWithSelect) poolExecutor.execute(new CacheTask(joinPoint, cacheTimeMin, key));
            return cacheValue;
        }
        //缓存未命中
        Object res = this.selectAndCache(joinPoint, cacheTimeMin, key);
        return res;
    }

    /**
     * @Author
     * @Date 2021/9/26 上午10:29
     * @Description 更新缓存异步任务
     */
    private class CacheTask implements Runnable {
        ProceedingJoinPoint joinPoint;
        String key;
        int cacheTime;

        CacheTask(ProceedingJoinPoint joinPoint, int cacheTime, String key) {
            this.joinPoint = joinPoint;
            this.cacheTime = cacheTime <= 0 ? 60 : cacheTime;
            this.key = key;
        }

        @Override
        public void run() {
            selectAndCache(this.joinPoint, this.cacheTime, this.key);
        }
    }

    /**
     * @Author
     * @Date 2021/9/26 上午10:43
     * @Description 查询并更新缓存
     */
    private Object selectAndCache(ProceedingJoinPoint joinPoint, int cacheTime, String key) {
        Object res = null;
        try {
            if (key == null || key.length() == 0) throw new NullPointerException("传入的 key 为空!");
            String uuid = UUID.randomUUID().toString();
            RedisLockUtil redisLock = new ProtoRedisLockUtil(stringRedisTemplate, 30000);
            //竞争分布式锁,防止短时间同时处理大量重复请求
            if (redisLock.lock(key, uuid)) {
                Object[] args = joinPoint.getArgs();
                res = joinPoint.proceed(args);
                if (res == null) throw new RuntimeException(key + "查询结果为空!");
                redisTemplate.opsForValue().set(key, res, Long.valueOf(cacheTime), TimeUnit.MINUTES);
                redisLock.unlock(key, uuid);
            }
        } catch (Throwable e) {
            logger.error("Redis 更新缓存异步任务异常:" + e.getMessage(), e);
        }
        return res;
    }

    /**
     * @Author
     * @Date 2021/9/26 上午10:47
     * @Description 计算 Redis 中的 Key
     */
    private String getRedisKey(String clazzName, String methodName, Object[] args) {
        StringBuilder key = new StringBuilder(clazzName);
        key.append(DELIMITER);
        key.append(methodName);
        key.append(DELIMITER);
        for (Object obj : args) {
            if (obj == null) continue;
            key.append(obj.getClass().getSimpleName());
            //参数不是基本数据类型时会缓存失效 TO-DO
            key.append(obj.toString());
            key.append(DELIMITER);
        }
        return key.toString();
    }

}

  切入点选择注解方式(使用灵活),自定义注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisCache {
    //缓存存活时间,单位:分钟
    int cacheTime() default 60;

    //是否缓存命中时也更新缓存,提高缓存命中率
    boolean refreshWithSelect() default false;
}

  最终使用:

 @RedisCache(cacheTime = 30, refreshWithSelect = true)
  public List<Object> me(String param) throws Exception {
     //正常查询数据库的业务代码  
 }

 

posted @ 2021-09-27 23:21  牛有肉  阅读(274)  评论(0编辑  收藏  举报