緩存擊穿及解決方案


  對redis了解比價淺,有錯誤之處請批評指正。
  場景:某服務查詢余額功能,根據用戶id查詢余額,如果該用戶在緩存中有余額,則直接返回緩存數據,如果沒有,則去數據庫查詢后返回並放入緩存;
  黑客采用ddos攻擊對網站進行飽和攻擊,用uuid生成用戶賬號進行查詢,由於隨機的uuid不是系統用戶,也就在緩存中無數據,導致每次查詢都是訪問數據庫,直接占用了所有數據庫連接;uuid不斷變化,無法進行固定攔截;如果將系統用戶全部取出進行比對,這個思路,我能想到的就是hashmap了,但hashmap默認加載因子是0.75,加上其數據所占的長度,空間利用率並不高,又因為數據量較大,會有查找較慢的問題;
  那有沒有什么辦法既占內存少,速度又快呢?有,bloom過濾器!
  概括來說,就是一個數組跟一組函數,數組的長度跟函數的多少都要跟誤判率一起經過算法確認,然后:
  1、先讓符合條件的數據進行填充,每條數據會被映射到數組的多個不同位置,將這些位置的數字由0改為1;
  2、多次重復后,該數組某些地方為1,某些地方為0;
  3、對於需要過濾的數據,用mightContain方法進行判斷,如果該數據所映射到數組上的所有點均為1,則通過驗證,否則不通過。
  PS:Bloom Filter可以手動設置誤判率, 誤判率必須大於0
-------------------------------------------------------------------
  Bloom Filter的實現,偉大的Google已經為我們准備好了, guava18以上版本包含此過濾器。
<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>19.0</version>
</dependency>
  Bloom Filter測試代碼:
  @Test
    public void bloomTest(){
        final int num = 100000;
        //初始化一個存儲string數據的bloom過濾器,初始化大小10w
        BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),num,0.001);
        Set<String> sets = new HashSet<>(num);
        List<String> lists = new ArrayList<String>(num);
        //向3個容器初始化10w隨機唯一字符串
        for(int i=0;i<num;i++){
            String uuid = UUID.randomUUID().toString();
            bf.put(uuid);
            sets.add(uuid);
            lists.add(uuid);
        }
        int wrong = 0;
        int right = 0;
        for(int i=0;i<10000;i++){
            String test = i%100 == 0? lists.get(i/100):UUID.randomUUID().toString();
            if(bf.mightContain(test)){
                if(sets.contains(test)){
                    right ++;
                }else{
                    wrong ++;
                }
            }
        }
        System.out.println("=============right================"+right);
        System.out.println("=============wrong================"+wrong);
    }

  輸出結果:

  =============right================100
  =============wrong================276

   多運行幾次,wrong的值大概在300左右變化,right一直為100。原因是對於的確在其中的數據,由於過濾器初始化的時候就采用的這些數據,所以對應的位置必定全部為1,不會誤判;但對於新數據,不一定全部不是1,所以存在誤判率;而誤判率默認為3%,因此,1w條數據誤判大概為300。我們調整誤判率,代碼改為:
BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),num,0.001);

  會發現wrong的數值在10上下波動,因為1w的千分之一正好是10。

-------------------------------------------------------------------
   以下我們使用Bloom Filter攔截非法請求:
  測試代碼,模擬2000請求同時訪問:
 int num = 2000;
 //倒數計數器
 CountDownLatch cdl = new CountDownLatch(num); 
  @Test
    public void testConcurrent() throws InterruptedException {
        System.out.println("執行開始啦");
        long start = System.currentTimeMillis();

        //真實數據
        List<String> unames = dbService.getAllUsers();
        for(int i=0;i<num;i++){
            String username = null;
            if(i%50==0){
                username = unames.get((int)(Math.random()*unames.size()));
            }else{
                username = UUID.randomUUID().toString();
            }
            Thread t = new MyThread(username);
            t.start();
            cdl.countDown();
        }
        Thread.sleep(10000);
    }

 MyThread代碼:

  private class MyThread extends Thread{
        private String name;
        public MyThread(String name){
            this.name = name;
        }

        @Override
        public void run() {
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            dbService.getSumScore(name);
        }
    }

  dbService.getSumScore(name)的代碼如下:

//獲取總成績
    public int getSumScore(String uname){
        //不在用戶列表,直接返回                           
        if(!bloomFilter.mightContain(uname)){
            System.out.println("================不在列表,直接返回=======================");
            return 0;
        }
        //可能在用戶列表,進行正常數據查詢
        int sumscore = 0;
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        String result = operations.get("uname");
        if(result != null){//緩存已有數據
            int order = count.incrementAndGet();
            System.out.println(uname+"================獲取數據來源:redis======================="+order);
            return Integer.valueOf(result);
        }else{//緩存無數據
            Map m = jdbcTemplate.queryForMap("select sum(score) as sumscore from student where name=?",new Object[]{uname});
            if(m != null){
                int order = count.incrementAndGet();
                System.out.println(uname+"================獲取數據來源:database======================="+order);
                if(null!=(m.get("sumscore"))){
                    sumscore = Integer.valueOf(m.get("sumscore")+"");
                    operations.set(uname,sumscore+"",1);
                }
            }
            return sumscore;
        }
    }

  當然,在service中過濾器是需要初始化的,初始化代碼如下:

  @PostConstruct
    private void init(){
        List<String> users = this.getAllUsers();
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),users.size(),0.001);
        for(String str:users){
            bloomFilter.put(str);
        }
    }

  輸出結果:

 
   可以看到,大部分的請求都被攔截了。在生產系統中,如果將此功能提出,在訪問我們的服務之前先預先對請求進行過濾,那么對於我們的某些接口便能提供很好的保護,有效降低系統被攻擊所承受的壓力。而且此方法對數據庫跟緩存依賴非常小,不用擔心對生產系統造成的破壞。
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM