前言:系统龟速运行,你以为加一个缓存就没事了?图样图森破!今天就说说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是可用的。
:关于redis集群的高可用、数据一致性算法、hash环等问题,这里再留一个坑,后面我会抽时间补上的。。。
作者:诸葛小猿
链接:https://xie.infoq.cn/article/2c826b2e6ddba5a36a65657f0
来源:InfoQ
InfoQ著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。