從.Net到Java學習系列目錄html
場景描述:咱們在項目中使用緩存一般都是先檢查緩存中是否存在,若是存在直接返回緩存內容,若是不存在就直接查詢數據庫而後再緩存查詢結果返回。這個時候若是咱們查詢的某一個數據在緩存中一直不存在,就會形成每一次請求都查詢DB,這樣緩存就失去了意義,在流量大時,可能DB就掛掉了。java
穿透:頻繁查詢一個不存在的數據,因爲緩存不命中,每次都要查詢持久層。從而失去緩存的意義。mysql
經常使用解決辦法:
①用一個bitmap和n個hash函數作布隆過濾器過濾沒有緩存的鍵。
②持久層查詢不到就緩存空結果,有效時間爲數分鐘。redis
我這裏使用的是雙重檢測同步鎖方式。sql
修改AreaService接口,添加以下兩個接口方法,selectAllArea2方法是可能會致使緩存穿透的方法。數據庫
List<Area> selectAllArea();
List<Area> selectAllArea2();
修改接口的實現類AreaServiceImpl:apache
@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,第一次訪問json
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是差很少的。