• 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 {
         //正常查询数据库的业务代码  
     }
    当你看清人们的真相,于是你知道了,你可以忍受孤独
  • 相关阅读:
    jquery插件课程2 放大镜、多文件上传和在线编辑器插件如何使用
    php课程 5-19 php数据结构函数和常用函数有哪些
    如何解决计算机显示文字乱码
    NSURLConnection使用
    UOJ #5. 【NOI2014】动物园 扩大KMP
    [ACM] n划分数m部分,它要求每一个部分,并采取了最大的产品(间隔DP)
    基于低压电力采集平台DW710C的基础开发
    eclipse 对齐行号在括号中显示和字体调整
    蜗牛—苍茫IT文章大学的路(十)
    国产与第三方库FFmpeg SDK
  • 原文地址:https://www.cnblogs.com/niuyourou/p/15345870.html
Copyright © 2020-2023  润新知