Redis系列之缓存穿透、缓存击穿、缓存雪崩、限流(八)

缓存穿透

问题描述

是指数据库中不存在的数据、redis缓存中不存在的key被大量请求。

本来正常情况下请求先发给redis,而redis中没有所需要的key,那么又会去数据库中找,如果有这个key就把该key更新到redis中并把结果返回给用户,如果数据库也没有这个key就正常提示用户没有即可。问题就是当大量的请求都是去查询在数据库中压根就不存在的数据(比如一共5000用户,用户id1-5000,请求是查询5001号用户数据),这样所有请求都会去过一遍数据库,redis便没有发挥承担请求压力的作用。若黑客利用此漏洞攻击进行攻击很有可能压垮数据库。

解决方案

1. 空值缓存

比如有人查询5001号用户数据,系统返回null,那么我们把<5001,null>缓存起来,这样下次查询5001号用户便不会打扰数据库。(但是这样黑客每次查询的用户id+1呢ಠ_ಠ)

2. 布隆过滤器

做法:将所有存在的数据hash到一个足够大的bitmaps中,每次要查询一个key时,先通过hash函数去bitmaps中查看是否命中,若未命中则说明要查询的这个key在数据库和缓存中肯定没有数据,就不用去打扰它们了;若命中了就说明很大可能这个key在数据库中是有相对应的数据的(为什么不是一定呢?因为hash冲突),就按之前的方法先去缓存查,有则返回查询结果,无则去数据库中查,有则更新缓存并返回结果,无则返回null,如下图。

缓存击穿

问题描述

比如现在有一个key非常热门,是查询热点,结果某一时刻这个key在redis缓存中过期了,那么此时大量关于该key的请求过来,并且这些请求在缓存中查不到就会全部打到数据库那边导致数据库被一瞬间压垮。

解决方案

1. 人工解决

派人实时监控热点数据,实时调整key的过期时长。

2. 使用锁

有时我们并不能精准定位热点数据,所以方案一还是有些不妥。那么我们可以用redis的分布式锁setnx:

Boolean lockKey = redisTemplate.opsForValue().setIfAbsent("lockKey", "123");

这样可以确保只有一个请求发送至数据库,不会给数据库太多压力。我们可以让其它请求进入等待状态,比如设置睡眠50ms再次去缓存中取,只要去数据库的那个请求查到了就会把这个k1对应的值更新至缓存中,其它请求就正常去缓存中取值了。

那么问题1来了,如果某线程获取锁成功却在后面执行业务逻辑代码出现异常导致删除锁的代码执行不到,那么其它线程也就会一直在50ms的循环中等待了,所以setnx还需要设置过期时间expire,但是同样的如果把expire操作放到setnx后面会出现线程安全问题导致expire执行不到,所以我们需要把setnx和expire写到一起(这是一个原子操作):

//setnx在key不存在时会去设置值value并返回1,存在就不会做任何操作并返回0
Boolean lockKey = redisTemplate.opsForValue().setIfAbsent("lockKey", "123", 3 , TimeUnit.SECONDS);

 现在每把锁都有手动释放(删除)和自动释放(过期),看起来万无一失了,但是在分布式的情况下还是会引入新的问题:如果线程1正常获取锁但是在执行业务代码时卡住了,此时到了过期时间线程1获取的锁自动释放,接着线程2就来正常获取锁并执行业务代码,但此时线程1恢复了,将业务代码执行完毕后便执行删除锁操作,那么线程1删除的是谁的锁呢?答案是线程2的,也就是说出现了我把别人的锁删掉的情况(就像你正在公厕方便,突然有人来把你的锁卸了,那么就有可能有另外的人来你的坑位...)。

解决方案

为了不让线程1能开我的锁,我要让每把锁有其对应的主人,只有其主人才能开自己这把锁,那么UUID不正好有唯一性吗,我们在获取锁时给锁的value设置这样一个值,然后释放锁之前判断UUID和这个锁的值是否相同就可以了:

public static void get(key) throws InterruptedException{
    //获取锁
    String uuid = UUID.randomUUID.toString();
    Boolean lockKey = this.redisTemplate.opsForValue().setIfAbsent("lockKey",uuid,3,TimeUnit.SECONDS);

    if(lockkey){//成功获取锁
        //去数据库中查找

        //释放锁前的判断
        if(uuid.equals(this.redisTemplate.opsForValue().get("lockKey"))){
            this.redisTemplate.delete("lockKey");
        }
    }else{
        TimeUnit.MILLISECONDS.sleep(50L);//睡50ms
        get(key);//重试
    }
}

缓存雪崩

问题描述

redis中许多key失效了(过期或者删除),此时又有大量并发请求打过来,redis中查不到就全跑到数据库那边去了,如此一来DB可能会被直接打垮。

缓存雪崩和缓存击穿挺像的,区别仅在于后者是某个热点key失效,而雪崩是很多key失效。

如何预防

缓存雪崩是在同一个时间点或是较短的时间段内缓存大量失效,那么我们可以给缓存的原有过期时间上再加一个随机值,只要尽量让这些缓存的过期时间避免大量重复即可。

另外我们也可以使用锁或队列,只要保证大量的线程不会同时对数据库进行读写即可,这种操作是以保护数据库为目的,代价则是用户的一些体验。(和上面缓存击穿说的很像,并且不适用于高并发的情况)

还有一种方案就是设置热点数据永不过期。

Redis限流 

漏桶算法 

原理:漏桶 (Leaky Bucket) 算法思路很简单,水 (请求) 先进入到漏桶里,漏桶以一定的速度出水 (接口有响应速率), 当水流入速度过大会直接溢出 (访问频率超过接口响应速率), 然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

利用 Redis-Cell 模块实现

Redis 4.0 提供了一个限流 Redis 模块,名称为 redis-cell,该模块提供漏斗算法,并提供原子的限流指令。

该模块只有一条指令 cl.throttle,下面看一下其参数和返回值

CL.THROTTLE test 100 400 60 3

test: redis key

100: 官方叫max_burst,其值为令牌桶的容量 - 1, 首次执行时令牌桶会默认填满

400: 与下一个参数一起,表示在指定时间窗口内允许访问的次数

60: 指定的时间窗口,单位:秒

3: 表示本次要申请的令牌数,不写则默认为 1

以上命令表示从一个初始值为100的令牌桶中取3个令牌,该令牌桶的速率限制为400次/60秒

返回值说明:

127.0.0.1:6379> CL.THROTTLE test 100 400 60 3
1) (integer) 0
2) (integer) 101
3) (integer) 98
4) (integer) -1
5) (integer) 0

1: 是否成功,0:成功,1:拒绝

2: 令牌桶的容量,大小为初始值+1

3: 当前令牌桶中可用的令牌

4: 若请求被拒绝,这个值表示多久后才令牌桶中会重新添加令牌,单位:秒,可以作为重试时间

5: 表示多久后令牌桶中的令牌会存满

令牌桶算法 

原理:令牌桶算法 (Token Bucket) 和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔 (如果 QPS=100, 则间隔是 10ms) 往桶里加入 Token (想象和漏洞漏水相反,有个水龙头在不断的加水), 如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token, 如果没有 Token 可拿了就阻塞或者拒绝服务。

利用 Redis String + 定时任务实现

利用定时任务不断增加令牌数,Redis->incr(),自增一,令牌桶满则不再增加;
来一个请求消耗一个令牌,Redis->decr(),自减一,当没有令牌时则拒绝请求。
一旦需要提高速率,则按需提高放入桶中的令牌的速率即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值