Previously
前兩篇文章(緩存穩定性 和 緩存正確性)跟大家討論了緩存的『穩定性』和『正確性』,緩存常見問題還剩下『可觀測性』和『規范落地&工具建設』
- 穩定性
- 正確性
- 可觀測性
- 規范落地和工具建設
上周文章發完之后,很多同學對我留的問題進行了深入的討論,我相信經過深度的思考,會讓你對緩存一致性的理解更加深刻!
首先,各個 Go 群和 go-zero 群里有很多的討論,但是大家也都沒有找到非常滿意的答案。
讓我們來一起分析一下這個問題的幾種可能解法:
-
利用分布式鎖讓每次的更新變成一個原子操作。這種方法最不可取,就相當於自廢武功,放棄了高並發能力,去追求強一致性,別忘了我之前文章強調過『這個系列文章只針對非追求強一致性要求的高並發場景,金融支付等同學自行判斷』,所以這種解法我們首先放棄。
-
把 A刪除緩存 加上延遲,比如過1秒再執行此操作。這樣的壞處是為了解決這種概率極低的情況,而讓所有的更新在1秒內都只能獲取舊數據。這種方法也不是很理想,我們也不希望使用。
-
把 A刪除緩存 這里改成設置一個特殊占位符,並讓 B設置緩存 用 redis 的
setnx
指令,然后后續請求遇到這個特殊占位符時重新請求緩存。這個方法相當於在刪除緩存時加了一種新的狀態,我們來看下圖的情況是不是又繞回來了,因為A請求在遇到占位符時必須強行設置緩存或者判斷是不是內容為占位符。所以這也解決不了問題。
那我們看看 go-zero 是怎么應對這種情況的,我們選擇對這種情況不做處理,是不是很吃驚?那么我們回到原點來分析這種情況是怎么發生的:
- 對讀請求的數據沒有緩存(壓根沒加載到緩存或者緩存已失效),觸發了DB讀取
- 此時來了一個對該數據的更新操作
- 需要滿足這樣的順序:B請求讀DB -> A請求寫DB -> A請求刪除緩存 -> B請求設置緩存
我們都知道DB的寫操作需要鎖行記錄,是個慢操作,而讀操作不需要,所以此類情況相對發生的概率比較低。而且我們有設置過期時間,現實場景遇到此類情況概率極低,要真正解決這類問題,我們就需要通過 2PC 或是 Paxos 協議保證一致性,我想這都不是大家想用的方法,太復雜了!
做架構最難的我認為是懂得取舍(trade-off),尋找最佳收益的平衡點是非常考驗綜合能力的。當然,如果大家有什么好的想法,可以通過群或者公眾號聯系我,感謝!
本文作為系列文章第三篇,主要跟大家探討『緩存監控和代碼自動化』
緩存可觀測性
前面兩篇文章我們解決了緩存的穩定性和數據一致性問題,此時我們的系統已經充分享受到了緩存帶來的價值,解決了從零到一的問題,那么我們接下來要考慮的是如何進一步降低使用成本,判斷哪些緩存帶來了實際的業務價值,哪些可以去掉,從而降低服務器成本,哪些緩存我需要增加服務器資源,各個緩存的 qps
是多少,命中率多少,有沒有需要進一步調優等。
上圖是一個服務的緩存監控日志,可以看出這個緩存服務的每分鍾有5057個請求,其中99.7%的請求都命中了緩存,只有13個落到DB了,DB都成功返回了。從這個監控可以看到這個緩存服務把DB壓力降低了三個數量級(90%命中是一個數量級,99%命中是兩個數量級,99.7%差不多三個數量級了),可以看出這個緩存的收益是相當可以的。
但如果反過來,緩存命中率只有0.3%的話就沒什么收益了,那么我們就應該把這個緩存去掉,一是可以降低系統復雜度(如非必要,勿增實體嘛),二是可以降低服務器成本。
如果這個服務的 qps
特別高(足以對DB造成較大壓力),那么如果緩存命中率只有50%,就是說我們降低了一半的壓力,我們應該根據業務情況考慮增加過期時間來增加緩存命中率。
如果這個服務的 qps
特別高(足以對緩存造成較大壓力),緩存命中率也很高,那么我們可以考慮增加緩存能夠承載的 qps
或者加上進程內緩存來降低緩存的壓力。
所有這些都是基於緩存監控的,只有可觀測了,我們才能做進一步有針對性的調優和簡化,我也一直強調『沒有度量,就沒有優化』。
如何讓緩存被規范使用?
了解 go-zero 設計思路或者看過我的分享視頻的同學可能對我經常講的『工具大於約定和文檔』有印象。
對於緩存來說,知識點是非常繁多的,每個人寫出的緩存代碼一定會風格迥異,而且所有知識點都寫對是非常難的,就像我這種寫了那么多年程序的老鳥來說,一次讓我把所有知識點都寫對,依然是非常困難的。那么 go-zero 是怎么解決這個問題的呢?
- 盡可能把抽象出來的通用解決方法封裝到框架里。這樣整個緩存的控制流程就不需要大家來操心了,只要你調用正確的方法,就沒有出錯的可能性。
- 把從建表 sql 到 CRUD + Cache 的代碼都通過工具一鍵生成。避免了大家去根據表結構寫一堆結構和控制邏輯。
這是從 go-zero 的官方示例 bookstore
里截的一個 CRUD + Cache 的生成說明。我們可以通過指定的建表 sql 文件或者 datasource 來提供給 goctl 所需的 schema,然后 goctl
的 model
子命令可以一鍵生成所需的 CRUD + Cache
代碼。
這樣就確保了所有人寫的緩存代碼都是一樣的,工具生成能不一樣嗎?😛
未完待續
本文跟大家一起討論了緩存的可觀測性和代碼自動化,下一篇我來跟大家分享一下我們是怎么提煉和抽象緩存的通用解決方法的,大家可以預先了解一下聚族索引的設計,自己先思考一下緩存該如何做,畢竟經過深度思考,你的理解會更加深刻嘛!
所有這些問題的解決方法都已包含在 go-zero 微服務框架里,如果你想要更好的了解 go-zero 項目,歡迎前往官方網站上學習具體的示例。
視頻回放地址
項目地址
https://github.com/tal-tech/go-zero
歡迎使用 go-zero 並 star 支持我們!
微信交流群
關注『微服務實踐』公眾號並點擊 交流群 獲取社區群二維碼。