閒魚如何計算實時優惠:兼顧可擴展、高併發與數據一致性
問題與挑戰
-
• 如何描述、存儲和計算優惠並提供較好的業務可擴展性
-
• 如何保障大流量下優惠實時計算的性能
-
• 爲優惠查詢加速做的數據同步如何實現一致性
本文的方案經過線上系統驗證,對於優惠系統設計的場景和數據同步的場景可做相應的參考。
背景
在我們日常生活中,常常會遇到下面這樣的場景:
在閒魚上,針對閒魚交易中的粉絲購買和粉絲回購的優惠促銷場景,提供了一種定向一口價的優惠能力:
-
賣家可以按商品分別面向全部粉絲、老粉、已購粉設置不同的優惠價格。
-
買家在導購、下單等場景可以實時看到自己能夠享受的最低優惠價格。
技術實現
我們通過三個步驟來實現
-
• 分解優惠的基本要素,實現優惠的基本表達和計算
-
• 爲了保障大流量下的優惠查詢下性能和業務的可擴展性,對優惠對象的判定過程進行抽象和加速
-
• 在優惠對象製備的過程中,通過離線 + 實時的方式同步數據,保障數據一致性
優惠的描述、存儲與計算
一個優惠主要描述了 “誰對哪個商品享受什麼優惠”,拆解爲三個要素就是:【優惠對象】+【優惠商品】+【優惠價格】。
在這個規則中,主要是要解決如何描述優惠對象:在粉絲優惠的場景下,優惠對象是指賣家的粉絲、賣家的已購粉絲等,在存儲一條優惠時,一個賣家的粉絲可以被描述爲 “賣家 ID_all_fans” 的符號(同理,已購粉絲是“賣家 ID_buy_fans”)。這樣我們可以得到一個優惠規則的描述大致如下:
【賣家 A_all_fans】+【商品 1234】+【18.88 元】,對應的業務語義是:賣家 A 的所有粉絲,對於(賣家 A 的)商品 1234,可以以 18.88 元的優惠價格成交。
以這條優惠爲例,當買家 B 訪問商品 1234 時,我們會執行這樣的一個過程
-
• 查詢商品 1234 上的優惠規則,發現一條【賣家 A_all_fans】+【商品 1234】+【18.88 元】的規則
-
• 分析【賣家 A_all_fans】表達的含義,表示的是賣家 A 的全部粉絲可以享受優惠
-
• 確定買家 B 是否是賣家 A 的粉絲,如果是,則以 18.88 元的價格展示優惠或者成交
這樣,我們就實現了優惠設置和計算的能力,這個時候,我們只需要這樣一個架構就可以實現:
優惠對象判定的抽象和加速 - 人羣
但這樣的架構存在兩個問題:
-
- 優惠計算過程需要解析【優惠對象】這個符號背後所包含的業務語義,再由系統進行判斷買家是否符合條件,隨着業務規則的升級,系統的會變的非常複雜,可擴展性差。
-
- 每一次優惠查詢,都需要訪問用戶的關注關係、購買關係,這整個查詢過程非常長,性能低下,當面對大流量時,系統會陷入癱瘓。
爲了解決這兩個問題,我們希望優惠計算過程不再需要理解【優惠對象】的語義,判定過程中也不要再去查詢各個業務系統。
我們發現,優惠對象的判定過程,都是在回答 “用戶是否屬於某個羣體”,我們可以將這個關係進行抽象,提前製備並存儲起來。在我們常見的技術手段中,表達一個用戶是否屬於某個羣體有兩種實現:
-
- 在用戶對象上打上一個標記。
-
- 創建一個 “人羣” 對象,將用戶關聯到人羣。
一般情況下,第一種方式使用於羣體較少可枚舉的情況,第二種方案適用於羣體較多的情況。在我們的實現中,使用了第二種方案。
我們將用於描述優惠對象的符號(例如 “賣家 A_all_fans”)作爲人羣的名稱去定義一個人羣,按照這個規則,我們爲每個賣家的不同分組各定義這樣一個人羣(這裏人羣作爲一個符號,這裏不需要實際被 “創建”)。
人羣和用戶的關係存儲可以通過 redis 實現,我們設計一個類似:${user_A}${crowd_B}的 key 寫入 redis。在查詢時,查詢 ${user_A}${crowd_B}這個 key 是否存在,就可以判定 user_A 是否屬於 crowd_B。(當然這是一種比較簡易的實現,實際設計中需要根據數據特性進行優化)。就這樣,我們定義了人羣的概念,並提供了一種實現人羣的技術方案,這個架構中,人羣在同時充當了 “協議” 和“緩存”的作用。
這時我們的得到的整體架構是這樣的(順帶緩存了一下優惠數據):
事實上,在我們基於中臺的解決方案中,從一開始面臨的就是這樣的架構(實際中臺的架構比這個會更復雜一些)。這裏我們嘗試從頭演進了這個系統,也得到這樣的一個方案。
在實際落地的過程中,我們核心要解決的問題,是如何將業務系統中的關注和購買關係同步到人羣中,並保證數據的一致性。
人羣同步的數據一致性
人羣的同步整體上分爲兩個主要部分:
-
將離線業務數據通過 T+1 的方式,同步到人羣服務中。
-
通過實時同步的方式,將當天實時產生的關注、取消關注等行爲產生的變動,同步的更新到人羣服務中。
這種結合的方式具有以下優點:
-
實時消費消息進行同步,保障了數據的實時性。
-
離線 T+1 的全量同步,保證實時同步過程中產生的數據不一致會被及時的糾正,保障了數據的最終一致。
-
離線同步解決了數據初始化過程中的全量同步問題。
但上述的兩個過程中,會出現兩類問題:
-
離線數據因爲其數據存儲的特徵,只會記錄存在的關注關係,如果是被刪除的關注關係(取消關注),則不會出現在離線數據中。因此實時同步中,因未同步取消關注事件產生了不一致,數據無法被全量同步糾正。
-
離線同步和實時同步在實際實施過程中,會產生一種常見的數據衝突:用戶 A 今天原本關注了用戶 B,某天較早的時候取消關注了,如果這個時候的離線數據還沒同步完成,全量同步會再次將 A 對 B 的關注關係寫入到人羣中,出現了與實際數據的不一致。
針對上述的兩個問題,分別給出了以下兩個解決方案:
-
針對取關數據誤差無法通過全量同步糾正的問題,同步過程中,寫入人羣的時候會添加一個過期時間,這個過期時間略長於離線全量同步的間隔,這樣的好處是一旦在實時同步過程中,出現了取關但未同步到人羣的情況,這條記錄會自動過期,從而避免了不一致的數據在系統中積累。
-
針對同步過程中發生數據衝突的問題,通過在實時同步的過程中,取關的事件在 redis 寫入一條臨時記錄,表示該數據近期發生過取關;在全量同步過程中,去比對 redis 中是否有取關記錄,避免發生衝突。
通過上述兩個解決方案,我們實現了人羣同步的最終一致性,最終實現的方式如圖:
這樣的同步方案,對於搜索、推薦等大流量的導購場景,提供了充分的數據一致性保障(絕大多數情況下,數據實時一致,對於小概率出現數據實時同步不一致,通過全量同步保障數據最終一致,滿足導購場景的一致性要求)。此外,針對交易這樣的要求強一致性但訪問規模較小的場景,我們通過下單前對人羣同步的數據進行覈對,保障數據的實時完全一致。
結語
本文從三個部分介紹了優惠的實現:
-
通過對優惠要素的拆解和人羣的定義,我們在描述、存儲和計算優惠的同時,提供較好的業務可擴展性。
-
通過提前製備人羣數據,我們保障了大流量下的優惠查詢下性能,系統能夠支持幾十萬 QPS 下的毫秒級響應。
-
在人羣同步的過程中,通過離線 + 實時的方式同步數據,保障了數據的最終一致性。
思考
在優惠的實現過程中,我們直接面臨了一個迭代了多年的優惠中臺,需要我們通過同步人羣數據的方式進行接入。可能一開始會疑惑爲什麼需要執行一個複雜、高成本且會引入數據一致性風險的同步過程。但當我們從業務的可擴展性、系統的性能角度從頭進行推演的時候,我們發現最終會回到類似的架構上來。可以說,在特定的業務規模下,架構的演進有它歷史的必然性。當然,也不是說這樣的架構是適用於所有情況的,如果我們在一個較小的規模下去快速驗證一個優惠能力,那麼可能最開始的架構是最合適的,架構選型還是需要結合實際情況出發量身定製。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3EX0NZwk3B6CzCzobyg85w