從.Net到Java學習第七篇——SpringBoot Redis 緩存穿透


從.Net到Java學習系列目錄

場景描述:我們在項目中使用緩存通常都是先檢查緩存中是否存在,如果存在直接返回緩存內容,如果不存在就直接查詢數據庫然后再緩存查詢結果返回。這個時候如果我們查詢的某一個數據在緩存中一直不存在,就會造成每一次請求都查詢DB,這樣緩存就失去了意義,在流量大時,可能DB就掛掉了。

穿透:頻繁查詢一個不存在的數據,由於緩存不命中,每次都要查詢持久層。從而失去緩存的意義。

常用解決辦法:
①用一個bitmap和n個hash函數做布隆過濾器過濾沒有緩存的鍵。
②持久層查詢不到就緩存空結果,有效時間為數分鍾。

我這里使用的是雙重檢測同步鎖方式。

修改AreaService接口,添加如下兩個接口方法,selectAllArea2方法是可能會導致緩存穿透的方法。

    List<Area> selectAllArea();
    List<Area> selectAllArea2();

修改接口的實現類AreaServiceImpl

  @Autowired
    private RedisService redisService;
    private JSONObject json = new JSONObject();

    /**
     * 從緩存中獲取區域列表
     *
     * @return
     */
    private List<Area> getAreaList() {
        String result = redisService.get("redis_obj_area");
        if (result == null || result.equals("")) {
            return null;
        } else {
            return json.parseArray(result, Area.class);
        }
    }

    @Override
    public List<Area> selectAllArea() {
        List<Area> list = getAreaList();
        if (list == null) {
            synchronized (this) {
                list = getAreaList(); //雙重檢測鎖
                if (list == null) {
                    list = areaMapper.selectAllArea();
                    redisService.set("redis_obj_area", json.toJSONString(list));
                    System.out.println("請求的數據庫。。。。。。");
                } else {
                    System.out.println("請求的緩存。。。。。。");
                }
            }
        } else {
            System.out.println("請求的緩存。。。。。。");
        }
        return list;
    }

    @Override
    public List<Area> selectAllArea2() {
        List<Area> list = getAreaList();
        if (list == null) {
            list = areaMapper.selectAllArea();
            redisService.set("redis_obj_area", json.toJSONString(list));
            System.out.println("請求的數據庫。。。。。。");
        } else {
            System.out.println("請求的緩存。。。。。。");
        }
        return list;
    }

運行程序,在瀏覽器中輸入地址http://localhost:8083/boot/getAll,第一次訪問

2018-06-22 10:21:24.730  INFO 10436 --- [nio-8083-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
請求的數據庫。。。。。。

刷新瀏覽器地址,第二次訪問

請求的緩存。。。。。。

再打開我們的redis可視化管理工具

在之前配置mysql數據庫連接的時候,由於沒有指定是否采用SSL,所以控制台會有一個警告信息,如下所示:

這個是因為使用的mysql版本比較高,要求開啟SSL,所以控制台會有一個警告,當然,你也可以忽略,如果要去除這個警告,可以在之前的mysql連接配置后面添加:&useSSL=false

  datasource:
    url: jdbc:mysql://localhost:3306/demo?&useSSL=false

刪除redis中的這個key值,我們通過使用一個並發測試工具來模擬緩存穿透的現象,這里使用到了jmeter這個並發測試工具。jmeter官網:  https://jmeter.apache.org/。jmeter更多使用教程:https://www.yiibai.com/jmeter/

將jmter下載到本地,然后解壓,雙擊jmeter.bat運行

(1)右鍵單擊“測試計划”,新建測試組

(2)新建HTTP請求

(3)保存並運行測試,這是時候其實已經在開始運行了,我們可以通過“選項"——“Log Viewer",來查看運行日志。

此時再查看IDEA中的控制台運行情況如下:

我們看到有四次進行了數據庫查詢,而我們想要的其實是只進行一次數據庫查詢,其它的都是直接從緩存中進行查詢。

重新刪除redis中的key值redis_obj_area,我們再來測試一下采用了雙重檢測同步鎖的方法selectAllArea2

修改jmeter中的請求路徑

然后運行,我們再看下IDEA中控制台中的記錄:

現在只有第一次是從數據庫中讀取了。

當然,如果我們不采用測試工具的話,我們也可以自己寫一個單元測試,來進行並發測試。

 單元測試類AreaServiceImplTest的代碼:

@RunWith(SpringRunner.class)
@SpringBootTest
public class AreaServiceImplTest {
    @Autowired
    public AreaService areaService;
    @Before
    public void setUp() throws Exception {

    }

    @Test
    public void selectAllArea() throws InterruptedException {
        final CountDownLatch latch= new CountDownLatch(4);//使用java並發庫concurrent
        //啟用10個線程
        for(int i=1;i<=10;i++){
            new Thread(new Runnable(){
                public void run(){
                    try {
                        //Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    areaService.selectAllArea();
                    System.out.println(String.format("子線程%s執行!",Thread.currentThread().getName()));
                    latch.countDown();//讓latch中的數值減一
                }
            }).start();
        }
        //主線程
        latch.await();//阻塞當前線程直到latch中數值為零才執行
        System.out.println("主線程執行!");
    }
    @Test
    public void selectAllArea2() throws InterruptedException {
        final CountDownLatch latch= new CountDownLatch(4);//使用java並發庫concurrent
        //啟用10個線程
        for(int i=1;i<=10;i++){
            new Thread(new Runnable(){
                public void run(){
                    try {
                        //Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    areaService.selectAllArea2();
                    System.out.println(String.format("子線程%s執行!",Thread.currentThread().getName()));
                    latch.countDown();//讓latch中的數值減一
                }
            }).start();
        }
        //主線程
        latch.await();//阻塞當前線程直到latch中數值為零才執行
        System.out.println("主線程執行!");
    }
    @Test
    public void selectAllArea3(){
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                areaService.selectAllArea2();
            }
        };
        ExecutorService executorService=Executors.newFixedThreadPool(4);
        for (int i=0;i<10;i++){
            executorService.submit(runnable);
        }
    }
}

運行結果,和使用jmeter是差不多的。


免責聲明!

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



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