從SpringBoot構(gòu)建十萬(wàn)博文聊聊緩存穿透,并發(fā)量過(guò)大該如何抗壓?
前言
在博客系統(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)問(wèn)題
- 惡意攻擊、爬蟲(chóng)造成大量空的命中,會(huì)對(duì)數(shù)據(jù)庫(kù)造成很大壓力
案例分析
由于文章的地址是這樣子的:
https://blog.52itstyle.top/49.html
大家很容易猜出,是不是還有 50、51、52 甚至是十萬(wàn)+?如果是正兒八經(jīng)的爬蟲(chóng),可能會(huì)讀取你的總頁(yè)數(shù)。但是有些不正經(jīng)的爬蟲(chóng)或者人,還真以為你有十萬(wàn)+博文,然后就寫(xiě)了這么一個(gè)腳本。
- for num in range(1,1000000):
- //爬死你,開(kāi)100個(gè)線程
解決方案
設(shè)置布隆過(guò)濾器,預(yù)先將所有文章的主鍵 ID 哈希到一個(gè)足夠大的 BitMap 中,每次請(qǐng)求都會(huì)經(jīng)過(guò) BitMap 的攔截,如果 Key 不存在,直接返回異常。這樣就避免了對(duì) Redis 緩存以及底層數(shù)據(jù)庫(kù)的查詢壓力。
這里我們使用谷歌開(kāi)源的第三方工具類(lèi)來(lái)實(shí)現(xiàn):
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>25.1-jre</version>
- </dependency>
編寫(xiě)布隆過(guò)濾器:
- /**
- * 布隆緩存過(guò)濾器
- */
- @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ù)量很大的情況下,效率如何呢?我們來(lái)做個(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("成功過(guò)濾到" + key);
- }
- long end = System.nanoTime();
- System.out.println("布隆過(guò)濾器消耗時(shí)間:" + (end - start));
- }
布隆過(guò)濾器消耗時(shí)間:281299,約等于 0.28 毫秒,匹配速度是不是很快?
錯(cuò)判率
萬(wàn)事萬(wàn)物都有所均衡,既然效率如此之高,肯定其它方面定有所犧牲,通過(guò)測(cè)試我們發(fā)現(xiàn),過(guò)濾器有 3% 的錯(cuò)判率,也就是說(shuō),本來(lái)沒(méi)有的文章,有可能通過(guò)校驗(yàn)被訪問(wè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));
- }
通過(guò)源碼閱讀,發(fā)現(xiàn) 3% 的錯(cuò)判率是系統(tǒng)寫(xiě)死的。
- public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
- return create(funnel, expectedInsertions, 0.03D);
- }
當(dāng)然我們也可以通過(guò)傳參,降低錯(cuò)判率。測(cè)試了一下,查詢速度稍微有一丟丟降低,但也只是零點(diǎn)幾毫秒級(jí)的而已。
- BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
那么如何做到零錯(cuò)判率呢?答案是不可能的,布隆過(guò)濾器,錯(cuò)判率必須大于零。為了保證文章 100% 的訪問(wèn)率,正常情況下,我們可以關(guān)閉布隆校驗(yàn),只有才突發(fā)情況下開(kāi)啟。比如,可以通過(guò)阿里的動(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)致的空命中率。雖然十萬(wàn)博客還沒(méi)有被百度收錄,每天也就寥寥的幾十個(gè)IP,但是夢(mèng)想還是有的,萬(wàn)一實(shí)現(xiàn)了呢?所以,還是要做好準(zhǔn)備的!