彻底搞懂缓存穿透,缓存击穿,缓存雪崩

在这里插入图片描述

一般用redis常规的写法:

1:客户端发起请求。
2:判断redis中是否有数据,如果有返回给客户端,没有则请求数据库,查出数据返回给客户端。

在这里插入图片描述

带来的问题一:缓存穿透

1:如果此时有人恶意的攻击呢?发起几十亿万条redis和mysql中都不存在的数据,请求访问你的网站,数据库不就挂了。

解决办法1:使用布隆过滤器

redis中没有数据,请求布隆过滤器拦截请求mysql和redis中不存在的key的请求。如果布隆过滤器中对应的key,在请求数据库,没有则返回一个非法访问
在这里插入图片描述

2:缓存空数据
在客户端请求redis时发现没有key,接着请求mysql发现也没有key,此时就把key缓存起来,value设置为null。(缺点只适合单一key多次访问数据库的情况

带来的问题二:缓存击穿

客户端访问数据的时候,redis中没有数据,mysql中有数据,相当于直接跳过了redis。

为何会发生

用户访问这条数据的时候,热点数据过期时间刚好到了。

问题

1:如果此时这条数据很热门,秒级有几十亿万次的访问量,数据库不就挂了。

解决

1:设置热点数据永远不过期。
2:发现redis中没有数据,加入分布式锁(拦截请求),接着查询redis,查询数据库后,把这条数据重新加入到缓存,释放锁,之后剩余的请求,请求redis时就可以查到数据了

Redisson 版本解决缓存击穿

public Item_kill getItemKill(int id) {
        /**
         * 布隆过滤器解决缓存穿透
         */
        if (!bloomFilter.mightContain(id)) {
            log.warn("非法秒杀商品id" + id);
            return null;
        }
        /**
         * 从缓存中获取秒杀商品的信息,如果此时热点数据恰好过期了呢?
         */
        Item_kill itemKill = (Item_kill) redisTemplate.opsForValue().get(CACHE_PRE + "[" + id + "]");
        if (itemKill != null) {
            return itemKill;
        }
        /**
         * 定义唯一标识key
         */
        String key = new StringBuffer().append(id).toString();
        RLock lock = redissonClient.getLock(key);
        boolean ok = false;
        Item_kill item_kill = null;
        try {
            ok = lock.tryLock(30, 10, TimeUnit.SECONDS);
            if(ok){
                /**
                 * 从缓存中获取秒杀商品的信息
                 */
                itemKill = (Item_kill) redisTemplate.opsForValue().get(CACHE_PRE + "[" + id + "]");
                if (itemKill != null) {
                    return itemKill;
                }
                item_kill = itemKillMapper.selectByPrimaryKey(id);
                /**
                 * 将热点数据重新加入到缓存,解决热点数据失效的问题
                 */
                redisTemplate.opsForValue().set(CACHE_PRE + "[" + id + "]",item_kill);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return item_kill;
    }

Jredis版本解决缓存击穿

/**
     * 解决缓存穿透(redis没有数据,db有数据,穿透一层)
     */
    @GetMapping("/test")
    public String test() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.err.println(Thread.currentThread().getName() + " : " + getKey("test"));
            }, "线程: " + i).start();
        }
        return "ok";
    }

    String getKey(String key) {
        Jedis jedis = jedisPool.getResource();
        String value = jedis.get(key);
        if (null == value) {
            Long setnx = jedis.setnx("setnx", "1");
            //加分布式锁,确保只有一个线程进行查数据库,其他加锁失败的线程重试 getKey()
            if (1 == setnx) {
                //查db
                System.err.println(Thread.currentThread().getName() + " 查db");
                value = "dbValue";
                //将 db 值更新回redis
                jedis.set(key, value);
                jedis.expire(key, 10);
                //释放分布式锁
                jedis.del("setnx");
            } else {
                System.err.println(Thread.currentThread().getName() + "竞争锁失败,重试查缓存");
                value = getKey(key);
            }
        }
        jedis.close();
        return value;
    }

模拟请求:可以看到10个请求打过来,只查一次 db,剩余的线程全部重试查询去了
在这里插入图片描述

通过对接口限流缓解缓存击穿

3:做接口的限流降级(举个例子) aop,单机版

@Aspect
@Component
public class limitAspect {
    private ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.zzh.service.aspects.limit.Limit)")
    public void limit() {

    }

    /**
     * @param
     * @method 对接口进行限流
     */
    @Around("limit()")
    public R before(ProceedingJoinPoint joinPoint) throws NoSuchMethodException, ClassNotFoundException {

        //获取被该注解作用的对象
        Object target = joinPoint.getTarget();

        //获取被该注解作用的对象名字
        String targetName = target.getClass().getName();
        //获取被该注解作用的对象的class
        Class<?> aClass = target.getClass();
        //获取请求的参数
        Object[] methodParam = joinPoint.getArgs();
        //获取被该注解作用的方法的名字
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
//        //根据参数,获取对应的参数类型
//        Class<?>[] argTypes = ReflectUtils.getClasses(methodParam);
//        //得到注解作用的方法了
//        Method method = aClass.getDeclaredMethod(methodName, argTypes);
//        //获取该方法上的Limit注解
//        Limit annotation = method.getAnnotation(Limit.class);
//        //获取该方法上设置的最大限流量
//        int i = annotation.maxLimit();
//        System.out.println("最大流量为:" + i);
        StringBuffer bufferKey = new StringBuffer().append(methodName).append(targetName);
        String key = String.valueOf(bufferKey);

        Method[] methods = aClass.getMethods();
        Limit annotation = null;
        //遍历所有的方法
        for (Method method : methods) {
            //根据获取到的方法名字,匹配获取该方法
            if (methodName.equals(method.getName())) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                //方法中的参数匹配,精确匹配方法
                if (parameterTypes.length == args.length) {
                    annotation = method.getAnnotation(Limit.class);
                }
            }
        }

        if (null != annotation) {
            Semaphore semaphore = semaphores.get(key);
            if (null == semaphore) {
                //semaphores.put()
                //初始化各个接口的访问流量
                //System.out.println("maxLimit:" + annotation.maxLimit());
                semaphores.putIfAbsent(String.valueOf(key), new Semaphore(annotation.maxLimit()));
                semaphore = semaphores.get(key);
            }
            try {
                //当达到最大的访问的流量后,只有等有空闲的流量时,别的人才能加入
                semaphore.acquire();
                //执行方法
                joinPoint.proceed();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
        return R.ok();
    }

}

自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(value = ElementType.METHOD)
public @interface Limit {
    //默认限制流量
    int maxLimit() default 10;
}

接口限流(这样的话,这个登录的接口就最大支持20个人同时访问了)

@RestController
public class logController {
    @PostMapping("/login")
    @Limit(maxLimit = 20)
    public R login() {

    }
}

三:缓存雪崩

介绍:

redis挂了,所有的请求都达到了数据库

解决

1:使用缓存集群,保证缓存高可用
使用 Redis Sentinel 和 Redis Cluster 实现高可用缓存

2:使用Hystrix
做一些限流或者熔断的兜底策略

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小咸鱼的技术窝

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值