當前位置:網站首頁>你的數據庫真的穿“防彈衣”了嗎

你的數據庫真的穿“防彈衣”了嗎

2022-05-14 21:24:48蘇州程序大白

博主介紹

作者主頁:蘇州程序大白

作者簡介:CSDN人工智能域優質創作者,蘇州市凱捷智能科技有限公司創始之一,目前合作公司富士康、歌爾等幾家新能源公司

如果文章對你有幫助,歡迎關注、點贊、收藏

有任何問題歡迎私信,看到會及時回複
關注蘇州程序大白,分享粉絲福利

前言

華强來到一家程序員商店買緩存,問程序員:“你這緩存保熟嗎?” …

在這裏插入圖片描述

緩存經常被用來减少數據庫訪問量,以此來提高系統性能,承受更多的並發請求,就像“防彈衣”一樣保護著數據庫,防止被一顆顆“請求子彈”擊中。
但引入緩存,也帶來了一些新的問題,比如緩存擊穿、緩存穿透、緩存雪崩、緩存數據一致性等問題。今天來聊聊緩存擊穿,百度一搜有很多相關的文章,但按照網上的一些教程去解决緩存擊穿,真的可以保證這一“防彈衣”不被擊穿嗎?

看一段示例代碼


public ResponseDTO getRoleById(Long id) throws Exception {
    
    String key = "User:Role:" + id;
    List<Long> data = (List<Long>) redisUtil.get(key);
    if (data == null) {
    
        data = userMapper.selectRoleIdByUserId(id);
        Long buffTime = (long) new Random().nextInt(30) * 60;
        redisUtil.set(key, data, buffTime);
    }
    return new ResponseDTO(Status.SUCCESS.code, "", data);
}

這段代碼使用redis做了緩存,只要緩存中有數據就不會去查數據庫,但如果緩存中沒數據,這時又恰好又大量請求來襲,那這些請求就會去訪問數據庫,如果並發請求量很大,數據庫就有可能被打死,這就是緩存擊穿。

解决思路也很簡單,只讓一個請求去查數據庫然後更新緩存,其他請求先等著,等查數據庫的兄弟更新完緩存我再去查緩存。具體實現也很容易,加個鎖不就好了。那一起來看看接下來這幾段代碼。

解决緩存擊穿的方法

public ResponseDTO getRoleById(Long id) throws Exception {
    
    String key = "User:Role:" + id;
    List<Long> data = (List<Long>) redisUtil.get(key);
    boolean isLock = false;
    ReentrantLock lock = new ReentrantLock();
    try {
    
        if (data == null) {
    
            if (lock.tryLock()) {
    
                isLock = true;
                data = userMapper.selectRoleIdByUserId(id);
                Long buffTime = (long) new Random().nextInt(30) * 60;
                redisUtil.set(key, data, buffTime);
            } else {
    
                Thread.sleep(100); // 此處僅為例子,具體由實際查詢情况定,也可以循環查詢幾次
                data = (List<Long>) redisUtil.get(key);
            }
        }
    }
    finally {
    
        if (isLock)
            lock.unlock();
    }
    return new ResponseDTO(Status.SUCCESS.code, "", data);
}

這個解决方案完全是一種錯誤的方案,因為這裏的鎖,鎖了個寂寞。

鎖是為了解决多線程問題的,即多個線程爭用一把鎖,誰搶到了誰用,但在SpringMVC中,一個請求就會建立一個線程,把鎖定義在方法中(ReentrantLock lock = … 那行代碼),那不就是一個線程一把鎖,我和自己搶,然後我鎖我自己嗎,鎖了個寂寞。所以這種方法無法防止緩存擊穿。

方法二

把鎖拿到外面定義並實例化,這樣就能做到所有線程用一把鎖。

static ReentrantLock lock = new ReentrantLock();
@Override
public ResponseDTO getRoleById(Long id) throws Exception {
    
    String key = "User:Role:" + id;
    List<Long> data = (List<Long>) redisUtil.get(key);
    boolean isLock = false;
    try {
    
        if (data == null) {
    
            if (lock.tryLock()) {
    
                isLock = true;
                data = userMapper.selectRoleIdByUserId(id);
                Long buffTime = (long) new Random().nextInt(30) * 60;
                // Thread.sleep(90); // 模擬用
                redisUtil.set(key, data, buffTime);
            } else {
    
                Thread.sleep(100);  // 此處僅為例子,具體由實際查詢情况定,也可以循環查詢幾次
                data = (List<Long>) redisUtil.get(key);
            }
        }
    }
    finally {
    
        if (isLock)
            lock.unlock();
    }
    return new ResponseDTO(Status.SUCCESS.code, "", data);
}

這樣看著可行,但又有了新問題,如果有兩個並發請求,請求A和請求B,A和B請求參數不同,即想要得到的數據也不同,並且此時緩存中也沒有A、B想要的數據,於是去查數據庫,假設此時A拿到了鎖,正在查數據庫,剛好B這時到達,因為A拿到了鎖還沒有釋放,導致B加鎖失敗,於是B睡眠然後等著查緩存。這時候A查完了數據,也更完了緩存,返回了正確的數據,過了一會,B睡醒了,但緩存中依然沒有B想要的數據,於是返回了null。

下面來複現一下這種情况,為了模擬這種並發情况,我們在查數據庫時也Thread.sleep()一下,模擬鎖還沒釋放,又有其他非同參的請求到達。

在這裏插入圖片描述
那使用ReentrantLock是不是無法解决緩存擊穿呢,倒也不是,可以維護一個ConcurrentHashMap,以方法名和請求參數為key,如果key存在數據且無法用已經存在的鎖成功加鎖,說明已經有其他相同請求線程在讀數據庫,然後就可以睡眠,稍後去讀緩存,如果沒有數據,則新建一把鎖,加鎖後讀數據庫、刷緩存。

方法三

用synchronized關鍵字加鎖。

public ResponseDTO getRoleById(Long id) throws Exception {
    
    String key = "User:Role:" + id;
    List<Long> data = (List<Long>) redisUtil.get(key);
   if(data == null) {
    
       synchronized (this) {
    
            data = (List<Long>) redisUtil.get(key);
            if(data != null) {
    
                return new ResponseDTO(Status.SUCCESS.code, "", data);
            }
            data = userMapper.selectRoleIdByUserId(id);
            Long buffTime = (long) new Random().nextInt(30) * 60;
            redisUtil.set(key, data, buffTime);
        }
   }
    return new ResponseDTO(Status.SUCCESS.code, "", data);
}

這種方法可以解决緩存擊穿問題,但會影響系統性能。比如此時緩存中無數據,然後有1000個請求同時到來,然後這1000個線程開始爭奪鎖,一個加鎖成功,去查了數據庫,更新緩存,剩下999個在等待鎖釋放再搶鎖,然後此時即使緩存有數據,這999個也是挨個加鎖、讀緩存,變成了一種串行執行,而不是並行讀緩存。只有接下來到達的其他請求,才是並行去讀取緩存。

方法四

使用分布式鎖。(個人認為這才是最佳解决方案)

public ResponseDTO getRoleById(Long id) throws Exception {
    
    String key = "User:Role:" + id;
    List<Long> data = (List<Long>) redisUtil.get(key);
   if(data == null) {
    
       // 加鎖,並設置過期時間為 30s,即超過30s自動解鎖
       if(redisUtil.setNx("getRoleById:" + id, 30L)) {
    
           data = userMapper.selectRoleIdByUserId(id);
           Long buffTime = (long) new Random().nextInt(30) * 60;
           redisUtil.set(key, data, buffTime);
           redisUtil.remove("getRoleById:"+id); // 解鎖
       } else {
    
           // 輪詢五次,每次間隔 100 ms 此處為例子,具體策略有具體情况定
           int count = 0;
           while(data == null && count < 5) {
    
                Thread.sleep(100);
                data = (List<Long>) redisUtil.get(key);
                count++;
           }
       }
   }
    return new ResponseDTO(Status.SUCCESS.code, "", data);
}

這種類似於ReentrantLock + ConcurrentHashMap解决方案,不同類請求加不同鎖,同類請求加鎖失敗就等待讀緩存。既保證了緩存無數據時到達的請求可以並發訪問更新後的緩存,又保證了不同參數的請求能讀到正確數據。

如果是單機部署,可以使用synchronized或者ReentrantLock + ConcurrentHashMap解决,但用synchronized會導致部分請求串行,性能較低。

如果要分布式部署,使用單機鎖也可以,畢竟部署幾十臺幾百臺,這點並發量數據庫還是扛得住的,但顯然使用分布式鎖更合適。

如果目前是單機部署,但考慮到未來可能會分布式部署,用redis做了緩存,那就用分布式鎖吧,畢竟欠下的技術債,總歸是要還的。

點擊直接資料領取

在這裏插入圖片描述

️關注蘇州程序大白公眾號️



版權聲明
本文為[蘇州程序大白]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/134/202205141816338400.html

隨機推薦