微服務架構下,靜態數據通用緩存機制!

Java技術棧

www.javastack.cn

打開網站看更多優質文章

本文轉自:波斯碼

鏈接:https://blog.bossma.cn/architecture/microservice-business-static-data-universal-cache-mechanism/

在分佈式系統中,特別是最近很火的微服務架構下,有沒有或者能不能總結出一個業務靜態數據的通用緩存處理機制或方案,這篇文章將結合一些實際的研發經驗,嘗試理清其中存在的關鍵問題以及探尋通用的解決之道。

什麼是靜態數據

這裏靜態數據是指不經常發生變化或者變化頻率比較低的數據,比如車型庫、用戶基本信息、車輛基本信息等,車型庫這種可能每個月會更新一次,用戶和車輛基本信息的變化來源於用戶註冊、修改,這個操作的頻率相對也是比較低的。

另外這類數據的另一個特點是要求準確率和實時性都比較高,不能出現丟失、錯誤,以及過長時間的陳舊讀。

具體是不是應該歸類爲靜態數據要看具體的業務,以及對變化頻率高低的劃分標準。在這裏的業務定義中,上邊這幾類數據都歸爲靜態數據。

爲什麼需要緩存

在面向用戶或車聯網的業務場景中,車型信息、用戶基本信息和車輛基本信息有着廣泛而高頻的業務需求,很多數據都需要對其進行關聯處理。在這裏緩存的目的就是爲了提高數據查詢效率。

靜態數據通常都保存在關係型數據庫中,這類數據庫的IO效率普遍不高,應對高併發的查詢往往捉襟見肘。使用緩存可以極大的提升讀操作的吞吐量,特別是KV類的緩存,沒有複雜的關係操作,時間複雜度一般都在O(1)。注意這裏說的緩存指內存緩存。

當然除了使用緩存,還可以通過其它手段來提高IO吞吐量,比如讀寫分離,分庫分表,但是這類面向關係型數據庫的方案更傾向於同時提高讀寫效率,對於單純提升讀吞吐量的需求,這類方案不夠徹底,不能在有限的資源情況下發揮更好的作用。

通用緩存機制

下面將直接給出一個我認爲的通用處理機制,然後會對其進行分析。

對於某個具體的業務,其涉及到六個核心程序:

  • 業務服務:提供對某種業務數據的操作接口,比如車輛服務,提供對車輛基本信息的增刪改查服務。

  • 關係數據庫:使用若干表持久化業務數據,比如SQLServer、MySQL、Oracle等。

  • 持久化隊列:可獨立部署的隊列程序,支持數據持久化,比如RabbitMQ、RocketMQ、Kafka等。

  • 緩存處理程序:從隊列接收數據,然後寫入緩存。

  • 數據一致處理程序:負責檢查緩存數據庫和關係型數據庫中數據是否一致,如果不一致則使用關係數據庫進行更新。

  • 緩存數據庫(Redis):支持持久化的緩存數據庫,這裏直接選了Redis,這個基本是業界標準了。

  • Java架構交流學習圈:874811168 面向1-3年經驗 Java開發人員 幫助突破瓶頸 提升思維能力

以及兩個外部定義:

  • 數據生產者:業務靜態數據的來源,可以理解爲前端APP、Web系統的某個功能或者模塊。

  • 數據消費者:需要使用這些業務靜態數據的服務或者系統,比如報警系統需要獲取車輛對應的用戶信息以便發送報警。

下面以問答的形式來說明爲什麼是這樣一種機制。

爲什麼需要業務服務?

既然是微服務架構,當然離不開服務了,因爲這裏探討的是業務靜態數據,所以是業務服務。不過爲了更好的理解,這裏還是簡單說下服務出現的原因。

當今業務往往需要在多個終端進行使用,比如PC、手機、平板等,既有網頁的形式,又有APP的形式,另外某個數據可能在多種不同的業務被需要,如果將數據操作分佈在多個程序中很可能產生數據不一致的情況,另外代碼不可避免的冗餘,讀寫性能更很難控制,變更也基本上是不敢變的。

通過一個業務服務可以將對業務數據的操作有序的管理起來,並通過接口的形式對外提供操作能力,代碼不用冗餘了,性能也好優化了,數據不一致也得到了一定的控制,編寫上層應用的人也舒服了。

爲什麼不是進程內緩存?

很多開發語言都提供了進程內緩存的支持,即使沒有提供直接操作緩存的包或庫,也可以通過靜態變量的方式來實現。對數據的查詢請求直接在進程內存完成,效率可以說是槓槓滴了。但是進程內緩存存在兩個問題:

  • 緩存數據的大小:進程可以緩存數據的大小受限於系統可用內存,同時如果機器上部署了多個服務,某個服務使用了太多的內存,則可能會影響其它服務的正常訪問,因此不適合大量數據的緩存。

  • 緩存雪崩:緩存同時大量過期或者進程重啓的情況下,可能產生大量的緩存穿透,過多的請求打到關係數據庫上,可能導致關係數據庫的崩潰,引發更大的不可用問題。

爲什麼是Redis?

Redis這類數據庫可以解決進程內緩存的兩個問題:

  • 獨立部署,不影響其它業務,還可以做集羣,內存擴容比較方便。

  • 支持數據持久化,即使Redis重啓了,緩存的數據自身就可以很快恢復。

另外Redis提供了很好的讀寫性能,以及方便的水平擴容能力,還支持多種常用數據結構,使用起來比較方便,可以說是通用緩存首選。關注微信公衆號:Java技術棧,在後臺回覆:redis,可以獲取我整理的 N 篇 Redis 教程,都是乾貨。

爲什麼需要隊列?

隊列在這裏的目的是爲了解耦,坦白的說這個方案中可以沒有隊列,業務服務在關係數據庫操作完成後,直接更新到緩存也是可以的。之所以加上這個隊列是由於當前的業務開發有很明顯的系統拆分的需求,特別是在微服務架構下,爲了降低服務之間的耦合,使用隊列是個常用選擇,在某些開發模型中也是很推崇的,比如Actor模型。

舉個例子,比如新註冊一個用戶,需要贈送其300積分,同時還要給其發個註冊成功的郵件,如果將註冊用戶、贈送積分、發成功郵件都寫到一起執行,會產生兩個問題:一是註冊操作耗時增加,二是其中某個處理引發整體不可用的機率增大,三是程序的擴展性不好;通多引入隊列,將註冊信息分別發到積分隊列和通知隊列,然後由積分模塊和通知模塊分別處理,用戶、積分、通知三個模塊的耦合降低了,相互影響變小了,以後再增加註冊後的其它處理也就是增加個隊列的事,整體的擴展性得到了增強。

隊列作爲一種常用的解耦方案,在緩存這裏雖然產生的影響不大,但是除了緩存難免同時還會有其它業務處理,所以爲了統一處理機制,這裏保留了下來。(既然用了,就把它發揚光大。)

爲什麼隊列需要持久化?

持久化是爲了解決網絡抖動或者崩潰導致數據丟失的問題,在數據從業務服務到隊列,隊列自身處理,再從隊列到緩存處理程序,中間都可能丟失數據。爲了解決丟失數據的問題,需要發送時確認、隊列自身持久化、接收時確認;但是需要注意確認機制可能會導致重複數據的產生,因爲在未收到確認時就需要重新發送或接收,而數據實際上可能被正常處理,只是確認丟失了;確認機制還會降低隊列的吞吐量,但是根據我們的定義業務靜態數據的變更頻率應該不高,如果同時還需要較高的併發分片是個不錯的選擇。

這裏持久化隊列推薦選擇RabbitMQ,雖然吞吐量支持的不是很大,但是各方面綜合不錯,併發夠用就好。

爲什麼需要數據一致檢查程序?

在業務服務操作完關係數據庫後,數據發送到隊列之前(或者不用隊列就是直接寫入緩存之前),業務服務崩潰了,這時候數據就不能更新到緩存了。還有一種情況是Redis發生了故障轉移,master中的更新沒有同步到slaver。通過引入這麼一個檢查程序,定時的檢查關係數據庫數據和緩存數據的差別,如果緩存數據比較陳舊,則更新之。這樣提供了一種極端情況下的挽救措施。

這個檢查程序的運行頻率需要綜合考慮數據庫壓力和能夠承受的數據陳舊時間,不能把數據庫查死了,也不能陳舊太久導致大量數據不一致。可以通過設置上次檢查時間點的方式,每次只檢查從上次檢查時間點(或者最近幾次,防止Redis故障轉移數據未同步的問題)到本次檢查時間點發生變更的數據,這樣每次檢查只對增量變更,效率更高。

同時需要理解在分佈式系統中,微服務架構下,數據不一致是經常出現的,必須在一致性和可用性之間做出權衡,盡力去降低影響,比如使用準實時或最終一致性。

只要數據一致檢查程序是不是就夠了?

假設沒有緩存處理程序,通過定時同步關係數據庫和緩存數據庫是不是就夠了呢?這還是取決於業務,如果是車型庫這種數據,增加一個新的車型,本來之前就沒有,時間上並不是很敏感,這個是可以的。但是對於新增了用戶或者車輛,數據消費者還是希望能夠馬上使用最新的數據進行處理,越快越好,這時使用同步或者準同步更新就能更加貼近需求。Java架構交流學習圈:874811168 面向1-3年經驗 Java開發人員 幫助突破瓶頸 提升思維能力

爲什麼不用緩存過期機制?

使用緩存過期機制可以不需要緩存處理程序和數據一致檢查程序,業務服務首先從Redis查詢數據,如果數據存在就直接返回,如果不存在則從關係數據庫查詢,然後寫入Redis,然後再返回,這也是一種常用的緩存處理機制,網上可以查詢到很多,很多人用的也很好。

但是緩存的過期時間是個問題:緩存多長時間過期,設置的短可以降低數據的陳舊,但是會增加緩存穿透的概率,即使採用隨機的緩存過期時間,在Redis重啓或者故障轉移的情況下還是會可能導致緩存雪崩,雪崩的情況下采用數據預熱機制,也可能會導致服務更長時間的不可用;設置的長可以提升緩存的使用率,但是增加了數據陳舊,在上邊對靜態數據的定義中對其準確率和實時性都有較高的要求,業務上能不能接受需要考慮。而且如果操作數據和查詢存在波動的峯谷,是不是要引入動態TTL的機制,以達到緩存使用和直接訪問數據庫的一種平衡,這就需要權衡業務需求和技術方案。

總結

通過上邊的這些問題問答,再來看看上面提出的微服務架構下靜態數據通用緩存處理機制。

  • 通過業務服務來包裝對數據的操作,不管是操作關係數據庫還是緩存數據庫,數據消費者其實不需要關心,它只關心業務服務能不能提供高併發實時數據的查詢能力。

  • 利用分佈式系統中經常使用隊列進行解耦的方式,業務服務不幹寫入緩存的事,增加一個隊列訂閱數據變更,然後從隊列取數據寫入緩存數據庫。

  • 對於絕大部分正常的情況,通過隊列更新緩存數據和業務服務中更新緩存數據,其實時性是差不多的,同時實現了業務操作和寫緩存的解耦。

  • 在極端崩潰導致數據不一致的情況下,通過數據一致檢查程序進行補救,儘快更新緩存數據。

  • 現在業務服務可以通過訪問Redis緩存來提供對靜態數據的高併發準實時查詢能力,緩存中不存在的數據就是不存在,沒有緩存穿透。

對於微服務架構而言,這個機制藉助隊列這種通用的解耦方式,獨立了緩存更新處理,通過準實時更新和定時檢查,保證了緩存的實時性和極端情況下較短時間內達到最終一致,通過緩存的持久化機制消除了緩存穿透和雪崩,在緩存的數據較大或讀取併發較高時支持水平擴容,可以認爲對業務靜態數據提供了一種廣泛適用的緩存處理機制。

這個方案在某些情況下可能是沒有必要的,比如你要緩存一個全國限行的城市列表,使用一個進程內緩存就夠了。

最後剩下的就是工作量的問題了,這個會給開發和維護帶來複雜性,隊列有沒有用的順手的,人手是不是夠,業務需求是什麼樣的,需要考慮清楚。

後記

Redis耦合問題:圖中業務服務直接訪問了Redis,如果要實現業務服務對Redis的完全透明,這個還比較複雜,可以考慮採用AOP的方式,對關係數據庫和Redis保持相同的類型定義,分別採用ORM和反序列化的方式標準化輸出,這是個想法我也沒有實現;同時緩存數據是準實時的,如果要求完全一致,還是應該提供從關係數據庫查詢的版本。另外如果要擺脫對Redis的直接依賴,還可以通過OpenResty來實現對資源的透明訪問,這個不是本文的重點。

服務可用性問題:這篇文章沒有關注服務可用性問題,爲了保證服務的高可用,每個服務或者程序都應該有多份部署的,無論是負載均衡的方案,或者傳統的主備方案,在部分部署不可用時仍能夠繼續提供服務。

寫的比較快,有些理解不免偏頗,歡迎指正。

最近熱文:

1、Spring Boot 2.3.1 發佈, 10 個新特性!

2、一週面試了 30 人,面到我心態爆炸…

3、求求你們別再寫滿屏的 try catch 了!

4、寫了個全局變量的bug,被同事們打臉!

5、Java 14 祭出神器,Lombok 被幹掉了?

6、爲什麼 Redis 單線程能達到百萬+QPS?

7、Spring Boot 2.3 優雅關閉新姿勢,真香!

8、Redis 到底是單線程還是多線程?

9、我天!xx.equals(null) 是什麼騷操作??

10、Struts2 爲什麼被淘汰?自己作死!

掃碼關注Java技術棧公衆號閱讀更多幹貨。

點擊「閱讀原文」帶你飛~

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