看完這篇,數據同步還不會,能怪誰

應用開發中,爲了提升查詢性能或者做服務降級方案時,我們會使用緩存作爲解決方案,像分佈式緩存方案,比如 Redis、Memcache等;本地緩存方案,比如 Guava、Caffeine等。如果僅僅對當前服務的執行結果的緩存,用於下次相同查詢時加快查詢效率來說,還相對簡單一點。只需要將查詢條件作爲key,返回的結果作爲 value 即可實現,複雜一點會加上緩存失效機制等。

但還有一種可能緩存,可能是需要進行數據的同步的操作的。比如筆者之前做過的用戶權限中心,由於對響應實時性方面有很大的要求,雖然使用了異步非阻塞編程方式以提高性能,但如果涉及到數據庫的操作,其實性能並不能達到目標值。由於權限的相關配置項通過字節估算,對資源消耗並不算大,因而,筆者考慮使用本地緩存方案實現。

同步方案

做數據同步需要考慮同步方案和數據格式。同步方案常見有主動同步(啓動初始化、定時任務)和被動同步(消息通知、回調)兩種模式。應用一般會在啓動的時候初始化一份基準數據,之後的數據更新都基於這份基準數據進行修改。對數據實時性要求不高的場景,可以通過定時任務方式主動拉取數據,在這種方式中存在全量和增量兩種模式。全量是最簡單的方案,只需要對原先的緩存進行清空操作,填充最新的數據即可,適合數據量比較小的場景。增量方式相對來說比較複雜,需要依照不同的更新維度做相應的修改。還是拿權限例子來說,一般存在Tenant、AppId、 User、Role、Group、Resource等內容,這裏存在層級關係, {User、Role、Group、Resource} 存在於 AppId 下,AppId 又同時存在於 Tenant 中,其中廣義上來說 API、Tag、Menu 都是屬於 Resource範疇,具體設計這裏不進行展開,那麼緩存格式可以是這樣的:

Tenant -> Appid -> Method -> Path -> UserId -> RoleId

在用戶登錄的時候,會攜帶 tenant、appid、user、role 等信息,同時,當前請求的 Method 和 Path 也是可以知曉的。假設用戶在配置請求路徑 Path 的時候配置錯誤了,現在需要在後臺進行修改,修改之後就會進行數據的同步,我們先不關心用哪種方式觸發同步,我們去修改緩存的時候,需要從左到右一層層進行判斷,進行修改,這樣還不是最麻煩的,麻煩的是上面的每一層級都是一個可以變化的單元,都可能存在新增、修改和刪除的情況,是不是想想就會覺得頭大了呢。那麼有哪些解決方案可以供參考:

  • 全量同步,簡單粗暴且高效,但不適合數據量大且獲取更新數據比較複雜麻煩的場景

  • 拆分多個緩存,例如 Tenant->Appid->Method->Path->RoleId, Tenant->Appid->Method->Path->UserId(這裏只是舉例說明,實際並非如此)

  • 簡化操作,一般緩存都是存在增刪改的操作,這三者中改操作往往是最複雜的一種,如果只有增刪會簡單很多

再回過來講一下消息通知的同步方式,消息通知存在 RabbitMQ、RocketMQ、Kafka 等消息中間件解決方案。在一致性方面要求高的場景,可以使用 RabbitMQ 和 RocketMQ,能確保數據量比較大的場景推薦使用 Kafka 方案,畢竟 Kafka 是爲大數據而生的。使用消息通知的方式就需要引用消息中間,相對 API 方式來說比較笨重且引入了一個不穩定因素,對於小項目來說得不償失,同時,如果是公司外部應用,不會提供消息中間件作爲數據同步方案。

接着說說回調,這種方式被廣泛用於對外業務中,HTTP 或者 HTTPS 方式比較輕量級、接受度高,當然回調這種概念不侷限於通訊協議方式,RPC 方式也是可以的。回調方式與消息通知方式進行對比的話,回調需要自行實現冪等和重試機制,在編碼方面需要投入更多,這也是大家爲什麼異步的場景青睞消息隊列的原因。

數據格式

數據格式需要結合同步方案和業務要求。如果是增量的方式,需要考慮修改前與修改之後,比如這樣:

[    {        "id":"UUID",        "op":"U", // 操作,U、D        "t":1590730661263, // 時間戳        "prev":{            "id":"XXX", // 更新前ID            "name":"zhansan", // 更新前名稱            "time":"1590730661124" // 更新前更新時間        },        "cur":{            "id":"XXX", // 更新後ID            "name":"lisi", // 更新後名稱             "time":"1590730661263" // 更新後時間        }    },    {     ......    }]

全量方式則不需要這麼複雜,只要最新結果集即可。同步方案的不同也會存在字段的考量,一般會從冪等性、數據一致性、服務穩定性、可用性、實時性等方面出發。一般我建議:

  • 字段儘可能短

  • 必須有id和時間戳信息

  • Type 類型字段值,儘可能使用 Int 類型或者短字符串映射,例如上面的op字段使用短字符串方式

一些建議

正向不行,可以試試反向。在設計緩存結構時候,由於人的大腦擅長正向思維,可能設計的結果並不特別的理想(在查詢和更新性能方面),這個時候可以考慮反向試試,可能會豁然開朗。Tenant->Appid->Method->Path->UserId 數據格式,在某些場景不如 UserId->Method->Path->Appid->Tenant

穩定節點在前,多變的在後。數據量少在前,數據量多在後。 上面的例子中,Tenant 相對比較穩定,變更的比較少且數據量相對於 UserId 肯定比較少。這樣在修改或者查找的時候,性能相對好。

空間與時間互換。 這個想必大家經常聽到,時間換空間或者空間換時間。對於性能有要求的業務場景,通過冗餘緩存方案可以提高查詢性能;在資源緊張的場景但對時間有包容性,那適當在實時性方面進行取捨。

不要忽視數據提供方的性能問題。 實時性不僅僅依賴於需要數據的那方或者中間件,數據提供方也是可能存在性能瓶頸的。如果數據的數據格式要求特別變態,需要數據提供方聯表查詢 3 張表以上,性能可想而知,所以同步的數據要進行取捨,從而節省網絡帶寬和IO,提升性能。

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