社区项目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);
    }
posted @ 2022-02-23 15:24  暮雪超霸  阅读(37)  评论(0编辑  收藏  举报