一、什么是缓存的穿透问题
如图,一个正常的请求一般都会经过cache层再到storage层,如果cache层没有而在storage层查到,则将数据新增到cache层后返回,下次再有同样的请求则直接从cache层返回数据,无需再请求storage层;而如果在storage层也获取不到数据,则没有数据新增到cache里,下次再有同样的请求会继续到达storage层,这就是缓存穿透的定义。
缓存的一个作用就是保护storage,也就是MySQL之类的数据库,而缓存穿透的话就失去了这一层保护。
二、解决方案
1、程序路由阶段就限定ID的范围(譬如使用正则)
2、程序硬编码判断ID值的合法性
3、数据库查询判断等
三、方案详情
假设有一张新闻表的ID范围是100到2000,这时如果查询2004或者3000等ID是没有的,但不代表未来没有。因此这里有其中一个做法(设置空键)如下:
1、譬如key=news2004,如果数据库查询返回空
2、那么我们依然往redis存储一个我们约定好的数据结构并设置过期时间,譬如设置为200秒(到底多少秒要根据实际情况来调整)
3、下次如果继续查到这个key,那么我们增加这个key的过期时间(譬如5秒)
以下为“伪代码”演示:
String newsID = getParameter("id");
News getNews = getFromRedis("news" + newsID);
if(getNews == null){
getNews = getFromDB("news" + newsID);
if(getNews == null){
getNews = defaultCache(); // 设置一个业务不可能出现的 默认固定缓存,譬如字符串 “-1”
}
setToRedis(getNews,200); // 塞入redis,初始过期时间为200秒
}else if(getNews.equal(defaultCache())){ // 如果取出的缓存是 默认缓存,则增加5秒的过期时间
expireCache("news" + newsID,50);
}
response.write(getNews);
接下来我们对以上方案进行一点优化,增加一个防护手段(封杀单IP):
如果有恶意用户进行反复请求,那么我们的redis里很快就会塞满各种无用的新闻key。数量过多,对数据库也会有压力(毕竟第一次还是要访问数据库的)
优化策略是:对于一个IP,我们单独开辟一个key/value,key=前缀+ip
1、如果DB没有命中,则 +2分
2、如果命中“默认值”, +1分
3、一旦有以上操作,对应key的数据重新expire指定的时间(譬如3600秒)
4、当该IP的分数值达到我们设定的阈值(譬如6分),则让该IP无法正常访问我们的新闻页面(或者给一个静态页面,类似于404默认页)
譬如有一个IP 192.168.10.20,我们将其塞入了缓存,但是一些稍微专业的“恶意用户”会更换IP,用192.168.10.9、192.168.10.8、192.168.10.101等继续恶搞。
对于这种情况我们需要设定封禁IP列表(封杀IP段),譬如192.168.10开头的IP一律不能访问。redis可用列表(List)解决这个问题,一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
三、方案优化 - 布隆过滤器
布隆过滤器是一种比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
适用于精确性要求不严格的场景,如上面的黑白名单、是否签到等。业务流程如下: