28.【實戰】在庫存服務中實現緩存與數據庫雙寫一致性保障方案

項目框架

庫存服務框架:spring boot+mybatis+jedis
學習項目地址: https://gitee.com/ddebug/my-eshop-inventory

整體流程

  1. 更新數據的時候,根據數據的唯一標識,將操作路由之後,發送到一個jvm內部隊列
  2. 讀取數據的時候,如果發現數據不在緩存中,那麼將重新讀取數據+更新緩存的操作,根據唯一標識路由之後,也發送同一個jvm內部的隊列中
  3. 一個隊列對應一個工作線程
  4. 每個工作線程串行拿到對應的操作,然後一條一條的執行
  5. 一個數據變更的操作,先執行刪除緩存,然後再去更新數據庫,但是還沒完成更新,此時如果一個讀請求過來,就會讀到空的緩存,
  6. 可以先將緩存更新的請求發送到隊列中,此時會在隊列中積壓,然後同步等待緩存更新完成

這裏有一個優化點,一個隊列中,其實多個讀請求更新緩存串在一起是沒意義的,因此可以做過濾,如果發現隊列中已經有一個更新緩存的請求了,那麼就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可

  1. 等內存隊列對應的工作線程完成了上一個操作的數據庫的修改之後,纔會去執行下一個操作,也就是緩存更新的操作,此時會從數據庫中讀取最新的值,然後寫入緩存中
  2. 如果請求還在等待時間範圍內,不斷輪詢發現可以取到值了,那麼就直接返回; 如果請求等待的時間超過一定時長比如200ms,那麼這一次直接從數據庫中讀取當前的舊值

項目代碼

根據切換標籤,查詢相應的代碼

在這裏插入圖片描述

1. 線程池+內存隊列初始化

  1. java web應用,做系統的初始化,一般在ServletContextListener 裏面做,listener,會跟着整個web應用的啓動,就初始化,類似於線程池初始化的構建
  2. spring boot應用,在Application啓動類裏,搞一個listener的註冊。

2. 兩種請求對象封裝

  1. 庫存更新對象: 刪除緩存+更新數據庫庫存
  2. 更新緩存請求: 讀爲空時,數據庫讀出來更新到緩存中

3. 請求異步執行Service封裝和請求處理的工作線程封裝

請求路由,根據每個請求的商品id,路由到對應的內存隊列中去,

主要實現同一個商品id的讀寫請求全部路由到同一個內存隊列中操作

private ArrayBlockingQueue<Request> getRoutingQueue(Integer productId) {
    RequestQueue requestQueue = RequestQueue.getInstance()// 先獲取productId的hash值
    String key = productId.toString()int h;
    int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)// 對hash值取模,將hash值路由到指定的內存隊列中
    // 比如內存隊列大小8,用內存隊列的數量對商品id對應hash值取模之後,結果一定在0-7之間;
    // 所以任何一個商品id都會被固定路由到同樣的一個內存隊列中去的
    int index = (requestQueue.queueSize() - 1) & hash;
    return requestQueue.getQueue(index)}

4. 兩種請求Controller接口封裝

庫存服務讀請求和更新請求

5. 讀請求去重優化

  1. 如果一個讀請求過來,發現前面已經有一個寫請求和一個讀請求了,那麼這個讀請求就不需要壓入隊列中了
  2. 因爲那個寫請求肯定會更新數據庫,然後那個讀請求肯定會從數據庫中讀取最新數據,然後刷新到緩存中,自己只要hang一會兒就可以從緩存中讀到數據了

6. 空數據讀請求過濾優化

  1. 可能某個請求數據商品id,在數據庫裏面壓根兒就沒有,那麼那個讀請求是不需要放入內存隊列的,而且讀請求在controller那一層,直接就可以返回了,不需要等待
  2. 或者說,我們也可以認爲每個商品有一個最最初始的庫存,但是因爲最初始的庫存肯定會同步到緩存中去的,有一種特殊的情況,就是說,商品庫存本來在redis中是有緩存的
  3. 但是因爲redis內存滿了,就給幹掉了,但是此時數據庫中是有值的
  4. 那麼在這種情況下,可能就是之前沒有任何的寫請求和讀請求的flag的值,此時還是需要從數據庫中重新加載一次數據到緩存中的

7. 深入的去思考優化代碼的漏洞

  1. 一個讀請求過來,將數據庫中的數據刷新到了緩存中,flag是false,然後過了一會兒,redis內存滿了,自動刪除了這個緩存
  2. 下一次讀請求再過來,發現flag是false,就不會去執行刷新緩存的操作了,而是hang在哪裏,反覆循環,等超過超時時間比如200ms,發現在緩存中始終查詢不到數據,然後就去數據庫裏查詢,就直接返回了

這種代碼,就有可能會導致,緩存永遠變成null的情況

  1. 最簡單的一種,就是在controller這一塊,如果在數據庫中查詢到了,就刷新到緩存裏面去,以後的讀請求就又可以從緩存裏面讀了

但這也會導致高併發下不一致問題,因爲我們是在controller中進行讀取數據庫存入redis裏的。所以需要下一步中的優化

8. 優化去重邏輯+redis緩存丟失讀請求不一致問題

  1. 接着上面優化,我們在controller中並不是查詢數據庫後直接刷新到緩存中,而是接着將該請求放入到內存隊列中,讓他排隊執行;

  2. 判斷讀請求重複執行不要在讀請求存入內存隊列之前執行,高併發下會出現標誌位併發異常問題;所有請求都存入內存隊列後,線程取出每個請求時,在進行是否重複判斷;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章