谈谈redis缓存三大问题(一)- 缓存穿透

这几天抽时间和大家一起聊聊redis缓存在生产环境使用中的几大问题,以及如何去优化代码去避免他!

首先我们来看看缓存使用过程的一个问题:缓存穿透

何为缓存穿透?
如下是一个正常的业务流程图:比如我一个正常的中国移动用户(非广告)进行一个业务请求,比如根据手机号去查账单信息,正常流程是,手机号输入进去,调用http请求,服务端收到请求后先根据手机号关键字去查询缓存,如果查到了直接返回,如果查不到去查一下数据库。试想一下移动这么大用户群体,如果每个人请求来了都直接查数据库,那么数据库压力可想而知!
在这里插入图片描述
那么这跟缓存穿透有什么关系呢?
如下:比如哪天老王觉得移动不爽了,搞个代理ip服务器,然后写个程序随机生成非移动手机号,然后去查 “账单” ,很明显这账单是不是一直不存在的,那么缓存是不是会一直查不到!很明显是,那么这么多请求都会全跑到数据库上,要是老王搞个几十台机器,整个浙江省移动的数据库怕是也扛不住这么大压力的!
在这里插入图片描述
如上只是一个简单的小案例说明下什么是缓存穿透!既然有问题,那么肯定是有解决方案的!
没错,现在业界用的比较多的,应对这种缓存穿透的方案就是使用过滤器!那么何为过滤器呢?
如下模型:首先讲数据库数据全部缓存起来,然后在查询缓存后,如果缓存不存在,先看下过滤器,如果过滤器中存在,说明数据库中有相关账号信息,可以进行查询操作,如果不存在,那么直接返回即可!
在这里插入图片描述
好!现在我们知道过滤器是什么了,那么就好办了,直接搞个jvm缓存不就好了!但是也不好办,一般热点数据,几十甚至上百G,直接缓存在服务器内存里,显然是不可能实现的!
那么怎么办呢!其实有方法,现在市面上也有很多成品的过滤器工具给我们直接使用,比如布隆过滤器!有一个google开发的叫guava的项目,就帮我们实现了布隆过滤器,我们拿下来可以直接使用,当然这个项目还有很多其他功能,这边不做赘述!
在这里插入图片描述
pom中直接引用如下依赖即可

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>
//设置bloom filter 初始化大小,可以
private static final Integer size = 1000000;

//设置bloom filter 的误判率,该该值必须设置,如果设为0会报错,误判率越低,消耗的内存越多,误判率越高,消耗的内存越少
private static final double fpp = 0.01;

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),size,fpp);

public static void main(String[] args) {
    for (int i=0;i<size;i++){
        bloomFilter.put(i);
    }
    List<Integer> result = new ArrayList<>();
    for (int i=size;i<size+10000;i++){
       if (bloomFilter.mightContain(i)){
           result.add(i);
       }
    }
    System.out.println(String.format("bloom filter error result size:[%s]",result.size()));
}

在这里插入图片描述
如上测试方法,我们定义bloom filter ,往里面插入1000000数据,然后设置误判率为0.01,然后去取10000个不在bloom filter里面的数据,发现取出来的数据是98个,这说明符合我们设置的误判率!在我们生产环境中,比如我们设置容错率0.01,那么10000个请求,只有十个最终会误查数据库,这个比例其实是完全可以接受的!

关于bloom工作原理,其实我们可以把他理解为我们java里面的list,只是这个list底层采用的是BitSet数组结构,Java BitSet可以按位存储,计算机中一个字节(byte)占8位(bit),而BitSet是位操作的对象,值只有0或1(即true 和 false),基本原理是,用1位来表示一个数据是否出现过,1表示出现过,0为没有出现过。使用用的时候既可根据某一个是否为0表示此数是否出现过。内部维护一个long数组,初始化只有一个long segement,所以BitSet最小的size是64;随着存储的元素越来越多,BitSet内部会自动扩充,一次扩充64位,最终内部是由N个long segement 来存储,默认情况下,BitSet所有位都是0即false;一个1G的空间,有 8102410241024=8.5810^9bit,也就是可以表示85亿个不同的数。这个数据量,相信对于绝大多数企业来说是完全够用的!

详细工作原理(模型)如下:
在这里插入图片描述
如图:
1代表bloom filter的bitset,初始值都是0,且支撑动态扩展(数组长度根据期望容错率来动态生成,期望容错率越低,需要的动态数组长度越大,消耗内存越多)
2代表向bloom filter中添加一个元素,三根线代表三个hash函数(hash函数个数可以根据期望容错率动态设置,期望容错率越低,hash函数越个数多,时间复杂度越高),每个hash计算出一个下标,然后将bitset中对应下标的位改成1
3,表示判断一个元素是否存在于bloom filter,当有一个值需要判断时,会使用跟put时候完全一样的三个hash函数,然后判断对应下标是否为1,只有都为1的时候,才认为可能存在(这里仅仅是可能存在,存在误判问题),如果只要有一个下标为0,则认为肯定不存在。
在这里插入图片描述
在这里插入图片描述
如上图,我添加大概20十几个value到bloom filter,然后右边做判断,可以看到图1,我1右边是没有添加过的,但是会判断是可能存在,原因就是因为:hash冲突。导致1计算的hash值下标都是1,。
但是图二,只要计算出来有一个下标值为0,则认为肯定不存在了。
在这里插入图片描述
如上图,在创建bloom filter的时候:com.google.common.hash.BloomFilter#create(com.google.common.hash.Funnel<? super T>, long, double, com.google.common.hash.BloomFilter.Strategy) 就会去根据你的期望容错率和期望数据量,计算一个初始化的bitset大小,以及需要hash的hash函数个数。

虽然bloom filter看起来好像是可以解决相关缓存穿透的问题,但是缺点也很明显:
1,维护麻烦,每次数据库数据新增修改,都需要维护bloom filter
2,只能新增不能删除,因为存在hash冲突,如果删除有可能存在误删(例:如果数值a1和a2计算结果都有一个共同的index,如果删除a1的时候需要吧index的位置置为0,这样相当于把a2也给删除了,所以是不可取的)
3,这种实现只支持jvm缓存,简单的说就是不支持分布式,等于如果你一个微服务有十台机器,那么同样的缓存就是10分完全一样的(后面找机会写写基于redis的分布式bloom过滤器)

好了关于缓存穿透的相关问题就先说到这,有不当之处欢迎留言指正!

发布了47 篇原创文章 · 获赞 97 · 访问量 6万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章