1. 程式人生 > 實用技巧 >快取與資料庫一致性問題深度剖析

快取與資料庫一致性問題深度剖析

前言

本篇文章是我之前系列文章中的一篇,主要討論了我們在平時的開發過程中,各大系統中都要用到的快取資料的問題,進一步延伸到資料庫和快取的雙寫一致性問題,並且給出了所有方案的實現程式碼方便大家參考。

本篇文章主要內容

  • 資料快取
    • 為何要使用快取
    • 哪類資料適合快取
    • 快取的利與弊
  • 如何保證快取和資料庫一致性
    • 不更新快取,而是刪除快取
    • 先操作快取,還是先操作資料庫
    • 非要保證資料庫和快取資料強一致該怎麼辦
  • 快取和資料庫一致性實戰
    • 實戰:先刪除快取,再更新資料庫
    • 實戰:先更新資料庫,再刪快取
    • 實戰:快取延時雙刪
    • 實戰:刪除快取重試機制
    • 實戰:讀取binlog非同步刪除快取

碼字不易,只求關注,歡迎關注我的原創技術公眾號:後端技術漫談(二維碼見文章底部)

專案原始碼在這裡

https://github.com/qqxx6661/miaosha

資料快取

在我們實際的業務場景中,一定有很多需要做資料快取的場景,比如售賣商品的頁面,包括了許多併發訪問量很大的資料,它們可以稱作是是“熱點”資料,這些資料有一個特點,就是更新頻率低,讀取頻率高,這些資料應該儘量被快取,從而減少請求打到資料庫上的機會,減輕資料庫的壓力。

為何要使用快取

快取是為了追求“快”而存在的。我們用程式碼舉一個例子。

我在自己的Demo程式碼倉庫中增加了兩個查詢庫存的介面getStockByDB和getStockByCache,分別表示從資料庫和快取查詢某商品的庫存量。

隨後我們用JMeter進行併發請求測試。(JMeter的使用請參考我之前寫的文章:

點選這裡

需要宣告的是,我的測試並不嚴謹,只是作對比測試,不要作為實際服務效能的參考。

這是兩個介面的程式碼:

/**
 * 查詢庫存:通過資料庫查詢庫存
 * @param sid
 * @return
 */
@RequestMapping("/getStockByDB/{sid}")
@ResponseBody
public String getStockByDB(@PathVariable int sid) {
    int count;
    try {
        count = stockService.getStockCountByDB(sid);
    } catch (Exception e) {
        LOGGER.error("查詢庫存失敗:[{}]", e.getMessage());
        return "查詢庫存失敗";
    }
    LOGGER.info("商品Id: [{}] 剩餘庫存為: [{}]", sid, count);
    return String.format("商品Id: %d 剩餘庫存為:%d", sid, count);
}

/**
 * 查詢庫存:通過快取查詢庫存
 * 快取命中:返回庫存
 * 快取未命中:查詢資料庫寫入快取並返回
 * @param sid
 * @return
 */
@RequestMapping("/getStockByCache/{sid}")
@ResponseBody
public String getStockByCache(@PathVariable int sid) {
    Integer count;
    try {
        count = stockService.getStockCountByCache(sid);
        if (count == null) {
            count = stockService.getStockCountByDB(sid);
            LOGGER.info("快取未命中,查詢資料庫,並寫入快取");
            stockService.setStockCountToCache(sid, count);
        }
    } catch (Exception e) {
        LOGGER.error("查詢庫存失敗:[{}]", e.getMessage());
        return "查詢庫存失敗";
    }
    LOGGER.info("商品Id: [{}] 剩餘庫存為: [{}]", sid, count);
    return String.format("商品Id: %d 剩餘庫存為:%d", sid, count);
}

首先設定為10000個併發請求的情況下,執行JMeter,結果首先出現了大量的報錯,10000個請求中98%的請求都直接失敗了。讓人很慌張~

開啟日誌,報錯如下:

SpringBoot內建的Tomcat最大併發數搞的鬼,其預設值為200,對於10000的併發,單機服務實在是力不從心。當然,你可以修改這裡的併發數設定,但是你的小機器仍然可能會扛不住。

將其修改為如下配置後,我的小機器才在通過快取拿庫存的情況下,保證了10000個併發的100%返回請求:

server.tomcat.max-threads=10000
server.tomcat.max-connections=10000

可以看到,不使用快取的情況下,吞吐量為668個請求每秒

使用快取的情況下,吞吐量為2177個請求每秒

在這種“十分不嚴謹”的對比下,有快取對於一臺單機,效能提升了3倍多,如果在多臺機器,更多併發的情況下,由於資料庫有了更大的壓力,快取的效能優勢應該會更加明顯。

測完了這個小實驗,我看了眼我掛著MySql的小水管騰訊雲伺服器,生怕他被這麼高流量搞掛。這種突發的流量,指不定會被檢測為異常攻擊流量呢~

我用的是騰訊雲伺服器1C4G2M,活動買的,很便宜。這裡打個免費的廣告,請騰訊雲看到後聯絡我給我打錢