• 社区项目Redis分布式锁穿透击穿雪崩


    项目利用使用到了redis,比如会出现穿透、击穿、雪崩等问题。

    穿透

    缓存种不存在,数据库种也不存在,导致每一次的请求都会到数据库层面。
    解决方案:缓存空对象,或者使用布隆过滤器

    击穿

    某个key在有大量的请求,但是大量请求到的时候,过期了,然后导致大量请求都到数据库层面
    解决方案:数据不过期或者使用分布式锁,防止所有的请求都到数据库

    雪崩

    缓存种的key大面积失效,导致所有请求都到了数据库。
    解决方案:在原有的缓存时间上,追加随机时间。避免同时失效

    实现分布式锁,并解决上述问题

    第一种方法:模板模式

    因为项目种使用redis缓存的地方比较多,如果每个地方都写一份代码,比较冗余。
    所以,采用了模板模式,将加锁、解锁操作都封装到抽象类种,使调用者只需要关系数据的处理即可;

    RedisLock类,构建加锁解锁骨架

    public abstract class RedisLock<T> {
        private static final String TAG = "RedisLock";
        private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
        private RedisTemplate redisTemplate;
        private T data;
    
    
        public RedisLock(RedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 枷锁
         *
         * @param key
         * @param timeUnit
         * @param timeout
         */
        public void lock(String key, TimeUnit timeUnit, long timeout) {
            Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, timeUnit);
            while (isLock == null || !isLock) {
                logger.warn("没有获取到锁");
                isLock =redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, timeUnit);
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //出现异常释放锁
    //                redisTemplate.delete(key);
                }
            }
            try {
                if (isLock) {
                    logger.warn("获取到锁");
                    //再次请求获取缓存,如果还是为空,则查询数据库。
                    //交给调用者处理
                    data = handler();
                }
            } catch (Exception e) {
                throw e;
            } finally {
                //解锁
                redisTemplate.delete(key);
                logger.warn("释放了锁");
            }
    
    
        }
        public T getData() {
            return data;
        }
        public abstract T handler();
    
    
    }
    

    在需要使用的地方初始化RedisLock

       @Override
        public ExchangePostsVO getOnePosts(Integer postId) {
            String postKey = RedisKeyUtil.getPostOneKey(postId);
            ExchangePosts record = (ExchangePosts) redisTemplate.opsForValue().get(postKey);
            String lockKey = "lock:post:" + postId;
            if (record == null) {
                RedisLock<ExchangePosts> redisLock = new RedisLock(redisTemplate) {
                    @Override
                    public ExchangePosts handler() {
                        //获取数据逻辑
                        ExchangePosts postObj = (ExchangePosts) redisTemplate.opsForValue().get(postKey);
                        if (postObj != null) {
                            return postObj;
                        }
                        postObj = getById(postId);
                        if (postObj == null) {
                            postObj = new ExchangePosts();
                            //创建空对象,防止缓存穿透
                        }
                        redisTemplate.opsForValue().set(postKey, postObj, 3+postObj.hashCode()%7, TimeUnit.HOURS);
                        return postObj;
                    }
                };
                redisLock.lock(lockKey, TimeUnit.MILLISECONDS, 100);
                record = redisLock.getData();
            }
    
            log.warn("存在数据");
            if (record.getId() == null) {
                throw new CustomException("记录不存在");
            }
            ExchangePostsVO vo = getExchangePostsVO(record);
            return vo;
        }
    

    第二种方法:AOP

    利用切面编程 AOP来实现代码的复用,自定义一个注解,在切面拦截此注解,从切面处获取自定义key的前缀并追加入参,构造成key。
    从缓存种获取到信息,判断信息是否为空,如果空,则创建分布式锁。如果拿到了锁,则从缓存种获取信息,如果有,则返回,没有则去数据库种获取信息,如果获取到的信息为null,则把空对象放入缓存,如果不为空,则把对象放入缓存。
    如果没有拿到锁,则休眠100毫米后再次尝试是否拿到了锁。如果拿到了,则从缓存种获取,没有则从数据库种获取。

    自定义注解

    /**
     * 自定义注解-实现Redis的数据缓存,并通过切面解决穿透、击穿、雪崩问题
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Cache {
        /**
         * 设置缓存的key的前缀
         * @return
         */
        String prefix() default "cache:";
    }
    

    切面

    package com.tute.edu.planetcommunity.lock;
    
    import com.tute.edu.planetcommunity.annotation.Cache;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import java.util.Arrays;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 处理redis缓存。解决穿透-切面
     */
    @Aspect
    @Component
    public class CacheAop {
        @Autowired
        private RedisTemplate redisTemplate;
        private String preLockKey = "lock:";
        /**
         * 环绕通知
         *
         * @param point
         */
        @Around("@annotation(com.tute.edu.planetcommunity.annotation.Cache)")
        public Object redisCache(ProceedingJoinPoint point) {
            MethodSignature methodSignature = (MethodSignature) point.getSignature();
            Class returnType = methodSignature.getReturnType();
            Cache annotation = methodSignature.getMethod().getAnnotation(Cache.class);
            //获取自定义的key前缀
            String prefix = annotation.prefix();
            //拼接我们的key
            String key = prefix + Arrays.asList(point.getArgs()).toString();
            Object obj = redisTemplate.opsForValue().get(key);
            if (obj == null) {
    
                //获取分布式锁
                String lockKey = preLockKey + key;
                Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
                while (isLock == null || !isLock) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                        isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    obj = redisTemplate.opsForValue().get(key);
                    if (obj != null) {
                        return obj;
                    }
                    //请求数据库,查询信息
                    obj = point.proceed(point.getArgs());
                    if (obj == null) {
                          obj = returnType.newInstance();
                    }
                    redisTemplate.opsForValue().set(key, obj, 3 + obj.hashCode() % 7, TimeUnit.HOURS);
                    return obj;
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                } finally {
                    redisTemplate.delete(lockKey);
                }
            } else {
                return obj;
            }
            try {
                return point.proceed(point.getArgs());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                return new Object();
            }
        }
    
    }
    
    

    使用

        @Override
        @Cache(prefix = "post:")
        public ExchangePosts getById(Serializable id) {
            return super.getById(id);
        }
    
  • 相关阅读:
    SpringMVC之数据处理及跳转
    SpringMVC之控制器Controller和RestFul风格
    第一个SpringMVC程序
    什么是SpringMVC
    Spring声明式事务
    【嘎】字符串-反转字符串
    【嘎】字符串-字符串中的单词数
    【嘎】二叉树-226. 翻转二叉树
    【嘎】数组-面试题 01.07. 旋转矩阵-看题解
    【嘎】字符串-统计位数为偶数的数字
  • 原文地址:https://www.cnblogs.com/chaoba/p/15927614.html
Copyright © 2020-2023  润新知