高并发问题(缓存雪崩,缓存穿透)



在Redis存储的所有数据中,有一部分是被频繁访问的。有两种情况可能会导致热点问题的产生,一个是用户集中访问的数据,比如抢购的商品,明星结婚和明星出轨的微博。还有一种就是在数据进行分片的情况下,负载不均衡,超过了单个服务器的承受能力。热点问题可能引起缓存服务的不可用,最终造成压力堆积到数据库。

出于存储和流量优化的角度,我们必须要找到这些热点数据。


热点数据发现


首先,Redis的缓存淘汰机制,能够留下那些热点的key,不管是LRU还是LFU
除了自动的缓存淘汰机制之外,怎么找出那些访问频率高的key呢?或者说,我们可以在哪里记录key被访问的情况呢?

  1. 客户端
    第一个当然是在客户端了,比如我们可不可以在所有调用了get,set方法的地方,加上key的计数。但是这样的话,每一个地方都要修改,重复的代码也多。如果我们用的是Jedis的客户端,我们可以修改Jedis的源码,在Jedis的Connection类的sendCommand()里面,用一个HashMap进行key的计数。
    但是这种方式有几个问题:姑同学院青
    1)会对客户端的代码造成入侵。
    2)不知道要存多少个key,可能会发生内存泄露的问题。
    3)只能统计当前客户端的热点key

  2. 代理层
    第二种方式就是在代理端实现,比如TwemProxy或者Codis,但是不是所有的项目都使用了代理的架构。

  3. 服务端
    第三种就是在服务端统计,Redis有一个monitor的命令,可以监控到所有Redis执行的命令。

    jedis. monitor(new JedisMonitor() {
    	@Override 
    	public void onCommand(String command){
    		System. out. println("# monitor: "+ command);
    	}
    });
    

    这种方法也会有两个问题:
    1)monitor命令在高并发的场景下,会影响性能,所以不适合长时间使用。
    2)只能统计一个Redis节点的热点key。

  4. 机器层面
    还有一种方法就是机器层面的,通过对TCP协议进行抓包,也有一些开源的方案,比如ELK的packetbeat插件。
    当我们发现了热点key之后,我们来看下热点数据在高并发的场景下可能会出现的问题,以及怎么去解决。


缓存雪崩


缓存雪崩就是Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候Redis请求的并发量又很大,就会导致所有的请求落到数据库。


缓存雪崩的解决方案


  1. 加互斥锁或者使用队列,针对同一个key只允许一个线程到数据库查询
  2. 缓存定时预先更新,避免同时失效
  3. 通过加随机数,使key在不同的时间过期
  4. 缓存永不过期

缓存穿透


数据在数据库和Redis里面都不存在,可能是一次条件错误的查询。
在这种情况下,因为数据库值不存在,所以肯定不会写入Redis,那么下一次查询相同的key的时候,肯定还是会再到数据库查一次。那么这种循环查询数据库中不存在的值,并且每次使用的是相同的key的情况,我们有没有什么办法避免应用到数据库查询呢?

(1)缓存空数据
(2)缓存特殊字符串,比如&&

我们可以在数据库缓存一个空字符串,或者缓存一个特殊的字符串,那么在应用里面拿到这个特殊字符串的时候,就知道数据库没有值了,也没有必要再到数据库查询了。

但是这里需要设置一个过期时间,不然的会数据库已经新增了这一条记录,应用也还是拿不到值。

这个是应用重复查询同一个不存在的值的情况,如果应用每一次查询的不存在的值是不一样的呢?即使你每次都缓存特殊字符串也没用,因为它的值不一样,比如我们的用户系统登录的场景,如果是恶意的请求,它每次都生成了一个符合ID规则的账号,但是这个账号在我们的数据库是不存在的,那Redis就完全失去了作用。

这种因为每次查询的值都不存在导致的Redis失效的情况,我们就把它叫做缓存穿透。


解决缓存穿透


关键在于我们怎么知道请求的key在我们的数据库里面是否存在,如果数据量特别大的话,我们怎么去快速判断。
这也是一个非常经典的面试题:
如何在海量元素中(例如10亿无序、不定长、不重复)快速判断一个元素是否存在?

如果是缓存穿透的这个问题,我们要避免到数据库查询不存的数据,肯定要把这10亿放在别的地方。为了加快检索速度,我们要把数据放到内存里面来判断。

问题来了:

如果我们直接把这些元素的值放到基本的数据结构(List,Map.Tree)里面,比如一个元素1字节的字段,10亿的数据大概需要900G的内存空间,这个对于普通的服务器来说是承受不了的。

所以,我们存储这几十亿个元素,不能直接存值,我们应该找到一种最简单的最节省空间的数据结构,用来标记这个元素有没有出现。

这个东西我们就把它叫做位图,他是一个有序的数组,只有两个值,0和1.0代表不存在,1代表存在。
在这里插入图片描述

那我们怎么用这个数组里面的有序的位置来标记这10亿个元素是否存在呢?

我们是不是必须要有一个映射方法,把元素映射到一个下标位置上?

对于这个映射方法,我们有几个基本的要求:

  1. 因为我们的值长度是不固定的,我希望不同长度的输入,可以得到固定长度的输出。
  2. 转换成下标的时候,我希望他在我的这个有序数组里面是分布均匀的,不然的话全部挤到一对去了,我也没法判断到底哪个元素存了,哪个元素没存。

这个就是哈希函数,比如MD5,SHA-1等等这些都是常见的哈希算法。
在这里插入图片描述
比如,这6个元素,我们经过哈希函数和位运算,得到了相应的下标。


哈希碰撞


这个时候,Tom和Mic经过计算得到的哈希值是一样的,那么再经过位运算得到的下标肯定是一样的,我们把这种情况叫做哈希冲突或者哈希碰撞。

如果发生了哈希碰撞,这个时候对于我们的容器存值肯定是有影响的,从数据结构和映射方法这两个角度来分析,我们可以通过哪些方式去降低哈希碰撞的概率呢?

第一种就是扩大维数组的长度或者说位图容量。因为我们的函数是分布均匀的,所以,位图容量越大,在同一个位置发生哈希碰撞的概率就越小。

是不是位图容量越大越好呢?不管存多少个元素,都创建一个几万亿大小的位图,可以吗?当然不行,因为越大的位图容量,意味着越多的内存消耗,所以我们要创建个合适大小的位图容量。

除了扩大位图容量,我们还有什么降低哈希碰撞概率的方法呢?

如果两个元素经过一次哈希计算,得到的相同下标的概率比较高,我可以不可以计算多次呢?原来我只用一个哈希函数,现在我对于每一个要存储的元素都用多个哈希函数计算,这样每次计算出来的下标都相同的概率就小得多了。

同样的,我们能不能引入很多个哈希函数呢?比如都计算100次,都可以吗?当然也会有问题,第一个就是它会填满位图的更多空间,第二个是计算是需要消耗时间的。

所以总的来说,我们既要节省空间,又要很高的计算效率,就必须在位图容量和函数个数之间找到一个最佳的平衡。

比如说:我们存放100万个元素,到底需要多大的位图容量,需要多少个哈希函数呢?

这个平衡就是布隆过滤器

更多推荐