閒魚如何計算實時優惠:兼顧可擴展、高併發與數據一致性

問題與挑戰

本文的方案經過線上系統驗證,對於優惠系統設計的場景和數據同步的場景可做相應的參考。

背景

在我們日常生活中,常常會遇到下面這樣的場景:

在閒魚上,針對閒魚交易中的粉絲購買和粉絲回購的優惠促銷場景,提供了一種定向一口價的優惠能力:

  1. 賣家可以按商品分別面向全部粉絲、老粉、已購粉設置不同的優惠價格。

  2. 買家在導購、下單等場景可以實時看到自己能夠享受的最低優惠價格。

技術實現

我們通過三個步驟來實現

優惠的描述、存儲與計算

一個優惠主要描述了 “誰對哪個商品享受什麼優惠”,拆解爲三個要素就是:【優惠對象】+【優惠商品】+【優惠價格】。

在這個規則中,主要是要解決如何描述優惠對象:在粉絲優惠的場景下,優惠對象是指賣家的粉絲、賣家的已購粉絲等,在存儲一條優惠時,一個賣家的粉絲可以被描述爲 “賣家 ID_all_fans” 的符號(同理,已購粉絲是“賣家 ID_buy_fans”)。這樣我們可以得到一個優惠規則的描述大致如下:

【賣家 A_all_fans】+【商品 1234】+【18.88 元】,對應的業務語義是:賣家 A 的所有粉絲,對於(賣家 A 的)商品 1234,可以以 18.88 元的優惠價格成交。 

以這條優惠爲例,當買家 B 訪問商品 1234 時,我們會執行這樣的一個過程

這樣,我們就實現了優惠設置和計算的能力,這個時候,我們只需要這樣一個架構就可以實現:

優惠對象判定的抽象和加速 - 人羣

但這樣的架構存在兩個問題:

    1. 優惠計算過程需要解析【優惠對象】這個符號背後所包含的業務語義,再由系統進行判斷買家是否符合條件,隨着業務規則的升級,系統的會變的非常複雜,可擴展性差。
    1. 每一次優惠查詢,都需要訪問用戶的關注關係、購買關係,這整個查詢過程非常長,性能低下,當面對大流量時,系統會陷入癱瘓。

爲了解決這兩個問題,我們希望優惠計算過程不再需要理解【優惠對象】的語義,判定過程中也不要再去查詢各個業務系統。 

我們發現,優惠對象的判定過程,都是在回答 “用戶是否屬於某個羣體”,我們可以將這個關係進行抽象,提前製備並存儲起來。在我們常見的技術手段中,表達一個用戶是否屬於某個羣體有兩種實現:

    1. 在用戶對象上打上一個標記。
    1. 創建一個 “人羣” 對象,將用戶關聯到人羣。

一般情況下,第一種方式使用於羣體較少可枚舉的情況,第二種方案適用於羣體較多的情況。在我們的實現中,使用了第二種方案。 

我們將用於描述優惠對象的符號(例如 “賣家 A_all_fans”)作爲人羣的名稱去定義一個人羣,按照這個規則,我們爲每個賣家的不同分組各定義這樣一個人羣(這裏人羣作爲一個符號,這裏不需要實際被 “創建”)。 

人羣和用戶的關係存儲可以通過 redis 實現,我們設計一個類似:${user_A}${crowd_B}的 key 寫入 redis。在查詢時,查詢 ${user_A}${crowd_B}這個 key 是否存在,就可以判定 user_A 是否屬於 crowd_B。(當然這是一種比較簡易的實現,實際設計中需要根據數據特性進行優化)。就這樣,我們定義了人羣的概念,並提供了一種實現人羣的技術方案,這個架構中,人羣在同時充當了 “協議” 和“緩存”的作用。

這時我們的得到的整體架構是這樣的(順帶緩存了一下優惠數據):

事實上,在我們基於中臺的解決方案中,從一開始面臨的就是這樣的架構(實際中臺的架構比這個會更復雜一些)。這裏我們嘗試從頭演進了這個系統,也得到這樣的一個方案。 

在實際落地的過程中,我們核心要解決的問題,是如何將業務系統中的關注和購買關係同步到人羣中,並保證數據的一致性。

人羣同步的數據一致性

人羣的同步整體上分爲兩個主要部分:

  1. 將離線業務數據通過 T+1 的方式,同步到人羣服務中。

  2. 通過實時同步的方式,將當天實時產生的關注、取消關注等行爲產生的變動,同步的更新到人羣服務中。

這種結合的方式具有以下優點:

  1. 實時消費消息進行同步,保障了數據的實時性。

  2. 離線 T+1 的全量同步,保證實時同步過程中產生的數據不一致會被及時的糾正,保障了數據的最終一致。

  3. 離線同步解決了數據初始化過程中的全量同步問題。

但上述的兩個過程中,會出現兩類問題: 

  1. 離線數據因爲其數據存儲的特徵,只會記錄存在的關注關係,如果是被刪除的關注關係(取消關注),則不會出現在離線數據中。因此實時同步中,因未同步取消關注事件產生了不一致,數據無法被全量同步糾正。

  2. 離線同步和實時同步在實際實施過程中,會產生一種常見的數據衝突:用戶 A 今天原本關注了用戶 B,某天較早的時候取消關注了,如果這個時候的離線數據還沒同步完成,全量同步會再次將 A 對 B 的關注關係寫入到人羣中,出現了與實際數據的不一致。

針對上述的兩個問題,分別給出了以下兩個解決方案: 

  1. 針對取關數據誤差無法通過全量同步糾正的問題,同步過程中,寫入人羣的時候會添加一個過期時間,這個過期時間略長於離線全量同步的間隔,這樣的好處是一旦在實時同步過程中,出現了取關但未同步到人羣的情況,這條記錄會自動過期,從而避免了不一致的數據在系統中積累。

  2. 針對同步過程中發生數據衝突的問題,通過在實時同步的過程中,取關的事件在 redis 寫入一條臨時記錄,表示該數據近期發生過取關;在全量同步過程中,去比對 redis 中是否有取關記錄,避免發生衝突。 

通過上述兩個解決方案,我們實現了人羣同步的最終一致性,最終實現的方式如圖:

這樣的同步方案,對於搜索、推薦等大流量的導購場景,提供了充分的數據一致性保障(絕大多數情況下,數據實時一致,對於小概率出現數據實時同步不一致,通過全量同步保障數據最終一致,滿足導購場景的一致性要求)。此外,針對交易這樣的要求強一致性但訪問規模較小的場景,我們通過下單前對人羣同步的數據進行覈對,保障數據的實時完全一致。

結語

本文從三個部分介紹了優惠的實現: 

思考

在優惠的實現過程中,我們直接面臨了一個迭代了多年的優惠中臺,需要我們通過同步人羣數據的方式進行接入。可能一開始會疑惑爲什麼需要執行一個複雜、高成本且會引入數據一致性風險的同步過程。但當我們從業務的可擴展性、系統的性能角度從頭進行推演的時候,我們發現最終會回到類似的架構上來。可以說,在特定的業務規模下,架構的演進有它歷史的必然性。當然,也不是說這樣的架構是適用於所有情況的,如果我們在一個較小的規模下去快速驗證一個優惠能力,那麼可能最開始的架構是最合適的,架構選型還是需要結合實際情況出發量身定製。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3EX0NZwk3B6CzCzobyg85w