1、什么是缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
如图1-1为简单的通过缓存提高系统吞吐量的系统,访问请求会先通过去访问缓存,缓存中未找到的时候再去查询数据库,(此处提及一下,优化到极致的mysql的数据库连接为300-700之间,300为机械硬盘数据,700为固态硬盘数据)
如果这个请求是一个比较消耗时间的sql,比如说电商中的查询总消费额这种,会消耗比较大的数据库资源,这种数据一般会放入缓存中,我们此处可以据一个例子sql:select sum(money) from order where userCode = '123';查询出来的数据,以userCode为key,以结果为value存入到缓存中,这种方法有一定的漏洞,如果我们找到一个根本就不存在的userCode,进行查询,这样的话每次查询都会绕过缓存,直接访问到数据库,只要我这边并发访问量稍微多一点,基本就能把mysql跑崩溃。
2、如何处理缓存穿透
处理缓存穿透的方法有很多,其实无外乎就是对数据进行过滤筛选,把真正有效的数据进行访问,无效数据直接过滤掉。
1、在某些特定场景下(登录),进行验证码,这种方法并不是很安全,因为目前图像识别技术在很大程度上能够解决验证码的问题,所以仅供简单使用。
2、布隆过滤器:这个是需要着重说明的一种方法,
首先我们需要对数据进行过滤,肯定要查询一下这个数据是否存在,比较简单的就是直接查询数据库,"select userCount from user where userCode = '123';",但是其实这种意义并不大,因为查询的过程其实也是要建立数据库链接的,唯一优化的是查询这种查询语句速度更快,对于并发的数据量处理速度可能会快一点,但是当黑客攻击并发量再高一点的时候,此时这种方式可能也就扛不住了。
此时我们该怎么处理呢?还是缓存,我们把代表数据标识位的数据放到缓存中,查询数据库之前先去缓存中查询一下是否存在如果数据存在的话,再做后续的操作。这种做法还算是比较有效的,但是唯一的缺点就是用户量比较大的时候讲所有的用户存储在内存中,对内存的负载压力太大,需要比较高的内存,很容易把内存撑爆。
或者我们可以通过solr等搜索引擎这种方式进行搜索,这种方式后面再说。
此处我们选用布隆过滤器进行查询,算法的简单图解如下图2-1:
因为我们只需要判断客户端传过来的userCode是否存在就可以,图中的hash1,hash2,hash3分别代表着三种不同的hash算法,你可以认为是md5,sha1,sha256等等,不同的userCode对应着不同的数据位,当需要校验的时候,判断每一种算法的得出来的byte位是否相同,只要一位不同,那么我们可以认为这个userCode不存在,直接返回就不存在就可以,这种方法就可以在很大程度上避免黑客攻击的情况。当然这种方法存在一定的风险,因为无论哪一种hash算法都会有hash碰撞,即使是几种算法在一块,仍会有这种可能(只不过是可能性非常小而已),在此需要根据具体的情况进行分析,而且一般情况下不能从布隆过滤器里面删除数据,还有就是创建布隆过滤器的时候byte数组的长度需要先确定初始化,这个数组长度和hash函数不太好确定。
关于布隆过滤器的代码,这里可以推荐一款比较好用的java库,Google Guava
在此附上一个例程代码
package com.kevin.demo;
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.util.*;
/**
* google guava实现 bloom filter
* bitArray,numHashFunction,funnel,Strategy,put()
* demo实例
* 场景描述:100W字符放入布隆过滤器中,另外随机生成1W字符串,判断他们在100W里面是否存在
*/
public class demo {
private static final int insertions =1000000;//100W
public static void main(String[] args) {
//创建一个布隆过滤器对象,初始化字符集和100W大小
BloomFilter bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),insertions);
//初始化一个存储String数据的set,初始化大小100W
Set sets =new HashSet(insertions);
//初始化一个存储String数据的list,初始化大小100W
List lists =new ArrayList(insertions);
//向三个容器中初始化100W个随机数据
for (int i =0; i
String uuid = UUID.randomUUID().toString();
bf.put(uuid);
sets.add(uuid);
lists.add(uuid);
}
int wrong =0;//纪录错误次数
int right =0;//纪录正确次数
// 随机生成1W字符串
for (int i =0; i <10000; i++) {
String test = i%100==0?lists.get(i/100):UUID.randomUUID().toString();//按照一定比例选择BF中肯定存在的
if(bf.mightContain(test)){
if(sets.contains(test)){
right++;
}else {
wrong++;
}
}
}
System.out.println("========right======="+right);
System.out.println("========wrong======="+wrong);
}
}