前言
在博客系統(tǒng)中,為了提升響應(yīng)速度,加入了 Redis 緩存,把文章主鍵 ID 作為 key 值去緩存查詢,如果不存在對(duì)應(yīng)的 value,就去數(shù)據(jù)庫(kù)中查找
。這個(gè)時(shí)候,如果請(qǐng)求的并發(fā)量很大,就會(huì)對(duì)后端的數(shù)據(jù)庫(kù)服務(wù)造成很大的壓力。
造成原因
* 業(yè)務(wù)自身代碼或數(shù)據(jù)出現(xiàn)問題
* 惡意攻擊、爬蟲造成大量空的命中,會(huì)對(duì)數(shù)據(jù)庫(kù)造成很大壓力
案例分析
由于文章的地址是這樣子的:
https://blog.52itstyle.top/49.html
大家很容易猜出,是不是還有 50、51、52
甚至是十萬+?如果是正兒八經(jīng)的爬蟲,可能會(huì)讀取你的總頁(yè)數(shù)。但是有些不正經(jīng)的爬蟲或者人,還真以為你有十萬+博文,然后就寫了這么一個(gè)腳本。
for num in range(1,1000000): //爬死你,開100個(gè)線程
解決方案
設(shè)置布隆過濾器,預(yù)先將所有文章的主鍵 ID 哈希到一個(gè)足夠大的 BitMap 中,每次請(qǐng)求都會(huì)經(jīng)過 BitMap 的攔截,如果 Key
不存在,直接返回異常。這樣就避免了對(duì) Redis 緩存以及底層數(shù)據(jù)庫(kù)的查詢壓力。
這里我們使用谷歌開源的第三方工具類來實(shí)現(xiàn):
<dependency> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <version>25.1-jre</version> </dependency>
編寫布隆過濾器:
/** * 布隆緩存過濾器 */ @Component public class BloomCacheFilter { public static
BloomFilter<Integer> bloomFilter = null; @Autowired private DynamicQuery
dynamicQuery; /** * 初始化 */ @PostConstruct public void init(){ String nativeSql
= "SELECT id FROM blog"; List<Object> list = dynamicQuery.query(nativeSql,new
Object[]{}); bloomFilter = BloomFilter.create(Funnels.integerFunnel(),
list.size()); list.forEach(blog
->bloomFilter.put(Integer.parseInt(blog.toString()))); } /** * 判斷key是否存在 *
@param key * @return */ public static boolean mightContain(long key){ return
bloomFilter.mightContain((int)key); } }
然后,每一次查詢之前做一次 Key 值校驗(yàn):
/** * 博文 */ @RequestMapping("{id}.shtml") public String
page(@PathVariable("id") Long id, ModelMap model) {
if(BloomCacheFilter.mightContain(id)){ Blog blog = blogService.getById(id);
model.addAttribute("blog",blog); return "article"; }else{ return "error"; } }
效率
那么,在數(shù)據(jù)量很大的情況下,效率如何呢?我們來做個(gè)實(shí)驗(yàn),以 100W 為基數(shù)。
public static void main(String[] args) { int capacity = 1000000; int key =
6666; BloomFilter<Integer> bloomFilter =
BloomFilter.create(Funnels.integerFunnel(), capacity); for (int i = 0; i <
capacity; i++) { bloomFilter.put(i); } /**返回計(jì)算機(jī)最精確的時(shí)間,單位納妙 */ long start =
System.nanoTime(); if (bloomFilter.mightContain(key)) {
System.out.println("成功過濾到" + key); } long end = System.nanoTime();
System.out.println("布隆過濾器消耗時(shí)間:" + (end - start)); }
布隆過濾器消耗時(shí)間:281299,約等于 0.28 毫秒,匹配速度是不是很快?
錯(cuò)判率
萬事萬物都有所均衡,既然效率如此之高,肯定其它方面定有所犧牲,通過測(cè)試我們發(fā)現(xiàn),過濾器有 3%
的錯(cuò)判率,也就是說,本來沒有的文章,有可能通過校驗(yàn)被訪問到,然后報(bào)錯(cuò)!
public static void main(String[] args) { int capacity = 1000000;
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),
capacity); for (int i = 0; i < capacity; i++) { bloomFilter.put(i); } int sum =
0; for (int i = capacity + 20000; i < capacity + 30000; i++) { if
(bloomFilter.mightContain(i)) { sum ++; } } //0.03 DecimalFormat df=new
DecimalFormat("0.00");//設(shè)置保留位數(shù) System.out.println("錯(cuò)判率為:" +
df.format((float)sum/10000)); }
通過源碼閱讀,發(fā)現(xiàn) 3% 的錯(cuò)判率是系統(tǒng)寫死的。
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long
expectedInsertions) { return create(funnel, expectedInsertions, 0.03D); }
當(dāng)然我們也可以通過傳參,降低錯(cuò)判率。測(cè)試了一下,查詢速度稍微有一丟丟降低,但也只是零點(diǎn)幾毫秒級(jí)的而已。
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),
capacity,0.01);
那么如何做到零錯(cuò)判率呢?答案是不可能的,布隆過濾器,錯(cuò)判率必須大于零。為了保證文章 100% 的訪問率
,正常情況下,我們可以關(guān)閉布隆校驗(yàn),只有才突發(fā)情況下開啟。比如,可以通過阿里的動(dòng)態(tài)參數(shù)配置 Nacos 實(shí)現(xiàn)。
@NacosValue(value = "${bloomCache:false}", autoRefreshed = true) private
boolean bloomCache; //省略部分代碼 if(bloomCache||BloomCacheFilter.mightContain(id)){
Blog blog = blogService.getById(id); model.addAttribute("blog",blog); return
"article"; }else{ return "error"; }
小結(jié)
緩存穿透大多數(shù)情況下都是惡意攻擊導(dǎo)致的空命中率。雖然十萬博客還沒有被百度收錄,每天也就寥寥的幾十個(gè)IP,但是夢(mèng)想還是有的,萬一實(shí)現(xiàn)了呢?所以,還是要做好準(zhǔn)備的!
源碼
https://gitee.com/52itstyle/spring-boot-blog
<https://gitee.com/52itstyle/spring-boot-blog>
熱門工具 換一換
感谢您访问我们的网站,您可能还对以下资源感兴趣:
调教肉文小说-国产成本人片免费av-空姐av种子无码-在线观看免费午夜视频-综合久久精品激情-国产成人丝袜视频在线观看软件-大芭区三区四区无码-啊啊好爽啊啊插啊用力啊啊-wanch视频网-国产精品成人a免费观看