高并发下Redis缓存穿透解决方案


文章目录

      • 1.为什么要使用数据缓存?
      • 2. 缓存穿透的解决方案

我们在web开发的时候以及其他需要缓存的地方都会使用到Redis,之前也写过Redis的入门文章,但是在本地调试写demo不容易看出redis的一些博大精深,于是就用这篇文章继续学习一下Redis的一些内容。
附上之前的那篇文章
SpringBoot整合Redis
然后是本篇文章涉及到的代码已上传至github
https://github.com/2NaCl/BloomFilter-demo
然后来开始我们这篇文章的内容:
1.为什么要使用数据缓存?
这时我们要思考两个问题:
  1. 为什么我们要把数据放入内存
  2. 为什么我们不用HashMap做缓存
其实这两个问题很简单,我泛泛来说一下,
第一题:
内存中读取快,而且具有线程之间的可见性,可以互相通信,避免计算的时间。
第二题:
虽然Mybatis底层很多HashMap,但是HashMap是因为在大并发下,HashMap为线程不安全的,
而且HashMap内的内容会随着服务中断丢失,但是Redis会保存起来持续生效,
而且HashMap只能在一个jvm进行,Redis支持多个工程
而且HashMap只能存储对象,Redis可以存储多种数据结构,且容量比HashMap大
而且HashMap横向扩展性比较低,Redis可以进行高并发的扩展
2. 缓存穿透的解决方案
首先先看一下我们Redis的缓存方案,工作流程。
第一种情况:在缓存存在的时候,首先Application先回去Redis寻找缓存,然后发现缓存在,直接获取数据返回
高并发下Redis缓存穿透解决方案
文章图片

第二种情况:当缓存不存在,数据库存在的时候,Application无法从Redis获取数据,那就去找数据库了,因为我们本身写的Redis代码就是一种Configuration,而持久层就是去找的数据库,所以发现数据库存在,就把寻找的信息写入缓存,方便下次查询,然后再把这次的查询结果返回。
那么,为什么我们查询数据库要加锁呢?是因为高并发下访问数据库会给数据库很大压力。
高并发下Redis缓存穿透解决方案
文章图片

第三种情况:数据库不存在,缓存穿透 。我们在高并发下,持续从数据库寻找数据,因为不存在,所以return null。
所以这种情况会给数据库很大压力,而且无意义,那么要如何解决呢?
  1. 将查询不存在的结果写入Redis
这种情况当然ok,但是如果恶意攻击,生成Random数据请求如何呢?
此时Redis是失效的,被打穿了,也就是缓存穿透。所以我们就要间接的从数据库中,从海量数据中查询一条数据是否存在。首先我们需要一个容器存储数据,其次需要一个高强度算法。
高并发下Redis缓存穿透解决方案
文章图片

在容器方面,我们首选bitMap,位图实现,将数据元素是否存在存入bitMap(java管bitMap叫bitSet)
在算法方面,我们首选Hash Function,这种算法基于哈希算法拥有将任意值经过计算得出分布均匀长度相等的结果的能力 (这里功能判断元素是否存在,存在就是1,不存在就是0)
但是也存在一些特殊情况,比如我们出现哈希碰撞可怎么办?
高并发下Redis缓存穿透解决方案
文章图片

有几种解决办法:
  1. 进行验算,存储多个hash,然后验算
  2. 增加hash值的长度
但是都有一定弊端,无法找到好的平衡点去衡量这个度。
高并发下Redis缓存穿透解决方案
文章图片

所以就有了一种算法,名叫布隆过滤器
布隆过滤器的本质是:一系列位数组(存放01),并且具备一系列随机映射函数
高并发下Redis缓存穿透解决方案
文章图片

步骤:
  1. 创建布隆过滤器,也就是一串的bitMap
  2. 然后用随机哈希函数计算出01存储,计算的次数是不一定的,要自己定义
  3. 计算多少次,验证也验证多少次,但是反过来就不成立,被三次验证为1的不一定存在,这也是布隆过滤器的一个特征,叫假阳性(false positive Pro),原理同样是哈希碰撞
  4. 如果验证的n次中,有一个0,也是说明不存在,因为有可能是其他数字的占位。
换句话说,哈希碰撞,不可能完全解决,只能尽力解决,所以某程抢票,有可能抢到一个座位的票,这就是哈希碰撞。
上面讲了原理,也可以尝试自己实现哈哈,另外这些是不需要通过redis的,因为这些只在特殊业务场景有用的。
如果想要实现布隆过滤器,可以使用谷歌开源的Guava做到,Guava可以对很多数据类型进行处理,并且生成其他种类的数据类型。
然后接下来讲解一下集成了布隆过滤器功能的Guava-Demo,代码会上传到github的,注意关注最上方的本人的github。
public class BloomFilterDemo {private static final int insertions = 1000000; public static void main(String[] args) {//初始化存储string数据的布隆过滤器,初始化大小为100w //默认误判率为0.03 BloomFilter> bf = BloomFilter.create( Funnels.stringFunnel(Charsets.UTF_8), insertions ); //用于存放所有实际存在的key,判断key是否存在 Set> sets = new HashSet<>(insertions); //用于存放所有实际存在的key,判断key是否存在 List> lists = new ArrayList<>(insertions); //向三个容器初始化100w个随机并且唯一的字符串 for (int i = 0; i < insertions; i++) { String uuid = UUID.randomUUID().toString(); bf.put(uuid); sets.add(uuid); lists.add(uuid); }int right = 0; //正确判断的次数 int wrong = 0; //错误判断的次数for (int i = 0; i < 10000; i++) { //可以被100整除的时候,取一个存在的数,否则随机生成一个UUID //0-10000之间,可以被100整除的有100个(100的倍数) String data = https://www.it610.com/article/i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString(); if (bf.mightContain(data)){ if (sets.contains(data)) { //判断是否存在,命中 right++; continue; } wrong++; } }NumberFormat percentFoemat = NumberFormat.getPercentInstance(); percentFoemat.setMaximumFractionDigits(2); //最大小数位数 float percent = (float)wrong / 9900; float bingo = (float) (9900 - wrong) / 9900; System.out.println("在100w个元素中,判断100个实际存在的元素,布隆过滤器认为存在的:" + right); System.out.println("在100w个元素中,判断9900个实际不存在的元素,误认为存在的:"+wrong+""+",命中率:" +percentFoemat.format(bingo)+",误判率:" +percentFoemat.format(percent)); }}

使用布隆过滤器还有一个好处就是可以减少所占用内存的量,700w数据也就只有0.8MB,具体算法可以看底层代码,这里就不多说了。
然后再说一下布隆过滤器的应用场景
高并发下Redis缓存穿透解决方案
文章图片

注意:以上情况只针对于缓存穿透的情况。一般情况下就老老实实用Redis
经过试验之后,会发现,假如同时有数十万的请求,访问数据库不同的不存在的信息,布隆过滤器就会起作用,查询,如果经过Bloom Filter查询cache存在的才会访问数据库,但其实这是假阳性的,最后呢,试验证明,还是存在一定误差的。
以下是证明这句话的Demo,代码已经放在github了,地址就在最上方。,代码注释很齐全了
【高并发下Redis缓存穿透解决方案】

    推荐阅读