Redis穿透、击穿、雪崩

解决高并发问题的其中一项措施是使用缓存,而通常的技术选型就是redis。

用户访问网站时,为了避免每次都到持久层(如mysql)中获取数据,可以先到缓存(如Redis)中获取;如果缓存中获取不到,才到数据库中获取,同时将获取到的数据缓存到redis中。加缓存的目的是让用户尽可能少的访问数据库,尽可能多的访问缓存数据,从而提高网站的响应速度,保证网站的高并发,保护持久层数据的安全,同时提升用户的体验。

在这里插入图片描述

 

 

 

 

有个黑帽子,一直使用订单id=-1的请求参数访问你的网站,会怎么样? 

缓存穿透 

比如数据中不存在id=-1的订单数据,如果请求查询这条数据,则缓存中查不到,会将请求打到下层的数据库上,这就是缓存穿透;

查询缓存中不存在的数据会导致缓存穿透。需要注意的是,低频的缓存穿透是不可避免的,但是需要避免高频的缓存穿透。

如果有人恶意并发访问数据库中不存在数据,就可能会导致数据库因扛不住大的并发而引起系统瘫痪!


解决方案一:缓存空对象

请求到redis,当redis没有命中该数据时,请求会到达mysql;如果mysql也不存在该数据时,则缓存一个空对象到redis中。这样就可以解决缓存穿透问题。

public String getOrderInfo(String orderId){
    
    // 查询缓存
    String orderInfoStr = redisClient.get(orderId);
    
    // 缓存不存在,查询数据库
    if(orderInfoStr == null){
    	OrderInfo orderInfo = orderMapper.selectByOrderId(orderId)
        if(orderInfo != null){
            // 数据库存在,则正常缓存
            orderInfoStr = JSON.toJSONString(orderInfo);
            redisClient.set(orderId,orderInfoStr,2L * 60 * 60 * 1000,TimeUnit.MILLISECONDS);
        }else{
            // 数据库存在,则短时间缓存空值
            orderInfoStr = "";
            redisClient.set(orderId,orderInfoStr,30 * 60 * 1000,TimeUnit.MILLISECONDS);
        }    
    }
    
    return orderInfoStr;
}

问题来了,当数据库中真的插入了该条数据,请求过来后只能拿到缓存中的空对象,该如何解决呢?

我们可以针对这种数据设置一个较短的过期时间,保证数据库和redis的弱一致性,就可以在一定程度上解决这个问题。

缓存空对象就没有其他问题了吗?no!!!

上面那个黑帽子使用订单id=-1的参数高并发请求你的系统,发现你的系统依然稳如狗,于是简单的改变了一下策略,随机生成负数订单id后高并发请求你的系统,你的系统会怎样?

这时,你的redis中会缓存大量的值为空的key,这会导致大量的内存占用,同时Redis有LRU或LFU的内存淘汰策略,可能会将缓存中有价值的数据淘汰掉,真正的用户请求过来会将请求打到数据库上。

所以,这种方式的缺点是:

  • 可能会缓存很多值为空的key,占用内存空间。同时Redis有LRU或LFU的内存淘汰策略,可能会将缓存中有价值的数据淘汰掉。
  • 对空值设置了时间,可能会导致数据库和redis中在某个时间段的数据不一致。

解决方案二:布隆过滤器 

没有什么问题是加一层不能解决的!如果有,就再加一层! 为了防止缓存穿透,可以将数据库中存在的id提前存放在一个List数组,后续,请求到达controller层,可以到List数组中查询这个id是否存在,如果存在则将请求往下发送,如果不存在,则直接返回。

懂门路的朋友马上就看出破绽了,如果数据库中的数据量很大,这个List数据会占用很大的空间,同时查询的效率也会降低,有没有解决办法呢?当然有!使用Bloom Filter。

Bloom Filter是一个占用空间很小、效率很高的随机数据结构,它由一个bit数组和一组Hash算法构成。可用于判断一个元素是否在一个集合中,查询效率很高,内节省内存空间。

Bloom Filter在这里留个坑,以后有机会详细再说。
需要注意的是,Bloom Filter存在哈希碰撞问题,有一定的错误率;但是有一点可以肯定的是:**Bloom Filter说这条数据存在,这条数据不一定存在;但是Bloom Filter说这条数据不存在,这条数据一定不存在。**要使用好Bloom Filter过滤器,要从bit数组和Hash算法的角度做优化。

在这里插入图片描述

 

缓存击穿 

缓存击穿是缓存穿透的特殊表现之一。一般的公司没有这样的业务,没有这样一条非常热的数据能够导致数据库崩溃,所以不需要解决。

当某个数据被高并发访问时,如果这个数据redis的key的突然失效,会导致这些请求同一时间打到数据库,数据库扛不住就会导致系统瘫痪。比如微博上突然爆出某个明星的出轨、结婚等消息,大家同时都到微博上搜这个明星的信息,这时如果这个热点key过期,就会导致微博挂掉。志玲姐姐结婚了,微博的程序猿已经够伤心的了,还要加班修复系统。

在这里插入图片描述

 

对于一般的公司来说,不会存在一条这样的热点数据,当这条数据失效的一瞬间将请求打到数据库从而导致系统崩溃的,所以也不用过度担心这个问题。

解决方案一:热点数据不过期 

最简单的方式就是,缓存这些热点数据的时候,不设置过期时间,这样就不用担心这个问题了。

新的问题又来了,当数据库中这些已有的数据发生变化了,缓存没有变,导致数据不一致,怎么办?

这就需要一种方式来保证数据库和redis中的数据的一致性。数据库和redis的强一致是很难做到的,但是弱一致性还是可以保证的。方式有很多,比如数据库中数据发生变化时,可以同步更新redis中的数据;可以通过相关的中间件监控mysql的binlog日志并更新redis等。

解决方案二:分布式锁

当热点key过期,允许这个热点key在redis中查询不到数据时将其中一个请求打到达数据库,但不是并发打到数据库,然后将数据缓存到redis,后面的请求就可以到redis中查询到数据了。

比如100W个人同时到微博查看志林解决结婚的这条信息,这时正好redis中的这个热点key过期,假设微博后端部署了10台服务器,如何保证这100w人中只有一个人的请求在查询不到缓存数据将请求打到数据库,并将查询到的数据缓存到redis,然后其他999999个请求都从缓存中拿数据呢?

显然JDK提供的java同步锁synchronized是不能实现的,因为这种方式只能在一个JVM中生效;分布式部署的多台机器就需要使用分布式锁。

分布式锁其实就是在外部存储空间一个标签,当多台机器同时需要访问某个相同资源,就需要去竞争这把锁,谁竞争到锁谁就有权利访问这个资源,其他的机器就需要等待,当这台机器访问完成后就释放锁,其他的机器继续竞争锁,以此类推。

分布式锁的实现也很多,最常用的就是zookeeper和redis。

缓存雪崩

缓存雪崩也是缓存穿透的特殊表现之一。

上面说的缓存击穿是一个热点key的失效,而缓存雪崩是多个热点key同时失效。

一般出现缓存雪崩的原因是:

  • 缓存过期的时间比较一致,某一时刻key大面积失效。解决办法:将缓存时间设置成一个随机数。
  • redis挂了,或因为网络抖动访问不了redis了。解决办法:使用redis集群。

解决方案一:数据预热,缓存时间随机 

这种方式比较简单,可以专门做一个mysql和redis数据的同步服务,项目启动时,使用同步服务将mysql中的基础数据比如商户、门店等相关信息加载到redis中,并设置随机的失效时间;同时再每隔一个固定的时间(比如6h)同步一次。

如果要求修改mysql后,及时同步到redis,就需要有其他的措施保障了。

解决方案二:redis集群

为了防止redis挂掉,就要使用redis集群做高可用,可以将数据进行分片,将同样的数据分布到多台机器上;当集群中的一台或几台机器宕机,也依然能保障redis是可用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值