vivo 全球商城:優惠券系統架構設計與實踐

vivo網際網路技術發表於2021-08-09

一、業務背景

優惠券是電商常見的營銷手段,具有靈活的特點,既可以作為促銷活動的載體,也是重要的引流入口。優惠券系統是vivo商城營銷模組中一個重要組成部分,早在15年vivo商城還是單體應用時,優惠券就是其中核心模組之一。隨著商城的發展及使用者量的提升,優惠券做了服務拆分,成立了獨立的優惠券系統,提供通用的優惠券服務。目前,優惠券系統覆蓋了優惠券的4個核心要點:創、發、用、計。

  • “創”指優惠券的建立,包含各種券規則和使用門檻的配置。

  • “發”指優惠券的發放,優惠券系統提供了多種發放優惠券的方式,滿足針對不同人群的主動發放和被動發放。

  • “用”指優惠券的使用,包括正向購買商品及反向退款後的優惠券回退。

  • “計”指優惠券的統計,包括優惠券的發放數量、使用數量、使用商品等資料彙總。

vivo商城優惠券系統除了提供常見的優惠券促銷玩法外,還以優惠券的形式作為其他一些活動或資產的載體,比如手機類商品的保值換新、內購福利、與外部廣告商合作發放優惠券等。

以下為vivo商城優惠券部分場景的展示:

二、系統架構及變遷

優惠券最早和商城耦合在一個系統中。隨著vivo商城的不斷髮展,營銷活動力度加大,優惠券使用場景增多,優惠券系統逐漸開始“力不從心”,暴露了很多問題:

  • 海量優惠券的發放,達到優惠券單庫、單表儲存瓶頸。

  • 與商城系統的高耦合,直接影響了商城整站介面效能。

  • 優惠券的迭代更新受限於商城的版本安排。

  • 針對多品類優惠券,技術層面沒有沉澱通用優惠券能力。

為了解決以上問題,19年優惠券系統進行了系統獨立,提供通用的優惠券服務,獨立後的系統架構如下:

優惠券系統獨立遷移方案

如何將優惠券從商城系統遷移出來,併相容已對接的業務方和歷史資料,也是一大技術挑戰。系統遷移有兩種方案:停機遷移和不停機遷移。

我們採用的是不停機遷移方案:

  • 遷移前,運營停止與優惠券相關的後臺操作,避免產生優惠券靜態資料。

靜態資料:優惠券後臺生成的資料,與使用者無關。

動態資料:與使用者有關的優惠券資料,含使用者領取的券、券和訂單的關係資料等。

  • 配置當前資料庫開關為單寫,即優惠券資料寫入商城庫(舊庫)。

  • 優惠券系統上線,通過指令碼遷移靜態資料。遷完後,驗證靜態資料遷移準確性。

  • 配置當前資料庫開關為雙寫,即線上資料同時寫入商城庫和優惠券新庫。此時服務提供的資料來源依舊是商城庫。

  • 遷移動態資料。遷完後,驗證動態資料遷移準確性。

  • 切換資料來源,服務提供的資料來源切換到新庫。驗證服務是否正確,出現問題時,切換回商城資料來源。

  • 關閉雙寫,優惠券系統遷移完成。

遷移後優惠券系統請求拓撲圖如下:

三、系統設計

3.1 優惠券分庫分表

隨著優惠券發放量越來越大,單表已經達到瓶頸。為了支撐業務的發展,綜合考慮,對使用者優惠券資料進行分庫分表。

關鍵字:技術選型、分庫分表因子

分庫分表有成熟的開源方案,這裡不做過多介紹。參考之前專案經驗,採用了公司中介軟體團隊提供的自研框架。原理是引入自研的MyBatis的外掛,根據自定義的路由策略計算不同的庫表字尾,定位至相應的庫表。

使用者優惠券與使用者id關聯,並且使用者id是貫穿整個系統的重要欄位,因此使用使用者id作為分庫分表的路由因子。這樣可以保證同一個使用者路由至相同的庫表,既有利於資料的聚合,也方便使用者資料的查詢。

假設共分N個庫M個表,分庫分表的路由策略為:

庫字尾databaseSuffix = hash(userId) / M %N

表字尾tableSuffix = hash(userId) % M

3.2 優惠券發放方式設計

為滿足各種不同場景的發券需求,優惠券系統提供三種發券方式:統一領券介面後臺定向發券券碼兌換髮放

3.2.1 統一領券介面

保證領券校驗的準確性

領券時,需要嚴格校驗優惠券的各種屬性是否滿足:比如領取物件、各種限制條件等。其中,比較關鍵的是庫存和領取數量的校驗。因為在高併發的情況下,需保證數量校驗的準確性,不然很容易造成使用者超領。

存在這樣的場景:A使用者連續發起兩次領取券C的請求,券C限制每個使用者領取一張。第一次請求通過了領券數量的校驗,在使用者優惠券未落庫的情況下,如果不做限制,第二次請求也會通過領券數量的校驗。這樣A使用者會成功領取兩張券C,造成超領。

為了解決這個問題,優惠券採用的是分散式鎖方案,分散式鎖的實現依賴於Redis。在校驗使用者領券數量前先嚐試獲取分散式鎖,優惠券發放成功後釋放鎖,保證使用者領取同一張券時不會出現超領。上面這種場景,使用者第一次請求成功獲取分散式鎖後,直至第一次請求成功釋放已獲取的分散式鎖或超時釋放,不然使用者第二次請求會獲取分散式鎖失敗,這樣保證A使用者只會成功領取一張。

庫存扣減

領券要進行庫存扣減,常見庫存扣減方案有兩種:

方案一:資料庫扣減。

扣減庫存時,直接更新資料庫中庫存欄位。

該方案的優點是簡單便捷,查驗庫存時直接查庫即可獲取到實時庫存。且有資料庫事務保證,不用考慮資料丟失和不一致的問題。

缺點也很明顯,主要有兩點:

1)庫存是資料庫中的單個欄位,在更新庫存時,所有的請求需要等待行鎖。一旦併發量大了,就會有很多請求阻塞在這裡,導致請求超時,進而系統雪崩。

2)頻繁請求資料庫,比較耗時,且會大量佔用資料庫連線資源。

方案二:基於redis實現庫存扣減操作。

將庫存放到快取中,利用redis的incrby特性來扣減庫存。

該方案的優點是突破資料庫的瓶頸,速度快,效能高。

缺點是系統流程會比較複雜,而且需要考慮快取丟失或當機資料恢復的問題,容易造成庫存資料不一致。

從優惠券系統當前及可預見未來的流量峰值、系統維護性、實用性上綜合考慮,優惠券系統採用了方案一的改進方案。改進方案是將單庫存欄位分散成多庫存欄位,分散資料庫的行鎖,減少併發量大的情況資料庫的行鎖瓶頸。

庫存數更新後,會將庫存平均分配成M份,初始化更新到庫存記錄表中。使用者領券,隨機選取庫存記錄表中已分配的某一庫存欄位(共M個)進行更新,更新成功即為庫存扣減成功。同時,定時任務會定期同步已領取的庫存數。相比方案一,該方案突破了資料庫單行鎖的瓶頸限制,且實現簡單,不用考慮資料丟失和不一致的問題。

一鍵領取多張券

在對接的業務方的領券場景中,存在使用者一鍵領取多張券的情形。因此統一領券介面需要支援使用者一鍵領券,除了領取同一券模板的多張,也支援領取不同券模板的多張。一般來說,一鍵領取多張券指領取不同券模板的多張。在實現過程中,需要注意以下幾點:

1)如何保證效能

領取多張券,如果每張券分別進行校驗、庫存扣減、入庫,那麼介面效能的瓶頸卡在券的數量上,數量越多,效能直線下降。那麼在券數量多的情況下,怎麼保證高效能呢?主要採取兩個措施:

a. 批量操作

從發券流程來看,瓶頸在於券的入庫。領券是實時的(非同步的話,不能實時將券發到使用者賬戶下,影響到使用者的體驗還有券的轉化率),券越多,入庫時與資料庫的IO次數越多,效能越差。批量入庫可以保證與資料庫的IO的次數只有一次,不受券的數量影響。如上所述,使用者優惠券資料做了分庫分表,同一使用者的優惠券資產儲存在同一庫表中,因此同一使用者可實現批量入庫。

b. 限制單次領券數量

設定閥值,超出數量後,直接返回,保證系統在安全範圍內。

2)保證高併發情況下,使用者不會超領

假如使用者在商城發起請求,一鍵領取A/B/C/D四張券,同時活動系統給使用者發放券A,這兩個領券請求是同時的。其中,券A限制了每個使用者只能領取一張。按照前述採用分散式鎖保證校驗的準確性,兩次請求的分散式鎖的key分別為:

使用者id+A_id+B_id+C_id+D_id

使用者id+A_id

這種情況下,兩次請求的分散式鎖並沒有發揮作用,因為鎖key是不同,數量校驗依舊存在錯誤的可能性。為避免批量領券過程中使用者超領現象的發生,在批量領券過程中,對分佈鎖的獲取進行了改造。上例一鍵領取A/B/C/D四張券,需要批量獲取4個分散式鎖,鎖key為:

使用者id+A_id

使用者id+B_id

使用者id+C_id

使用者id+D_id

獲取其中任何一個鎖失敗,即表明此時該使用者正在領取其中某一張券,需要自旋等待(在超時時間內)。獲取所有的分散式鎖成功,才可以進行下一步。

介面冪等性

統一領券介面需保證冪等性(冪等性:使用者對於同一操作發起的一次請求或者多次請求的結果是一致的)。在網路超時、異常情況下,領券結果沒有及時返回,業務方會進行領券重試。如果介面不保證冪等性,會造成超發。冪等性的實現有多種方案,優惠券系統利用資料庫的唯一索引來保證冪等。

領券最早是不支援冪等性的,表設計沒有考慮冪等性。

那麼第一個需要考慮的問題:在哪個表來新增唯一索引呢?

無非兩種方案:現有的表或者新建表。

  • 採用現有的表,不需要增加表的關聯。但如上所述,因為做了分庫分表,大量的表需要新增唯一欄位,並且需要相容歷史資料,需要保證歷史資料新增欄位的唯一性。

  • 採用新建表這種方式,不需要相容歷史資料,但缺陷也很明顯,增加了一層表的關聯,對效能和現有邏輯都有很大影響。綜合考慮,我們選取了在現有表新增唯一欄位這種方式,這樣更利於保證效能和後續的維護性。

第二個考慮的問題:怎麼相容歷史資料和業務方?歷史資料增加了唯一欄位,需要填入唯一值,不然無法新增唯一索引。我們採用指令碼刷資料的方式,構造唯一值並重新整理到每一行歷史資料中。優惠券已對接的業務方沒有傳入唯一編碼,針對這種情況,優惠券側生成唯一編碼作為替代,保證相容性。

3.2.2 定向發券

定向發券用於運營在後臺針對特定人群進行發券。定向發券可以彌補使用者主動領券,人群覆蓋不精準、覆蓋面不廣的問題。通過定向發券,可以精準覆蓋特定人群,提高下單轉化率。在大促期間,大範圍人群的定向發券還可以承載活動push和降價促銷雙重任務。

定向發券主要在於人群的圈選和發券流程的設計,整體流程如下:

定向發券不同於使用者主動領券,定向發券的量通常會很大(億級)。為了支撐大批量的定向發券,定向發券做了一些優化:

1)去除事務。事務邏輯過重,對於定向發券來說沒必要。發券失敗,記錄失敗的券,保證失敗可以重試。

2)輕量化校驗。定向發券限制了券型別,通過限制配置的方式規避需嚴格校驗屬性的配置。不同於使用者主動領券校驗邏輯的冗長,定向發券的校驗非常輕量,大大提升發券效能。

3)批量插入。批量券插入減少資料庫IO次數,消除資料庫瓶頸,提升發券速度。定向發券是針對不同的使用者,使用者優惠券做了分庫分表,為了實現批量插入,需要在記憶體中先計算出不同使用者對應的庫表字尾,資料歸集後再批量插入,最多插入M次,M為庫表總個數。

4)核心引數可動態配置。比如單次發券數量,單次讀庫數量,發給訊息中心的訊息體包含的使用者數量等,可以控制定向發券的峰值速度和平均速度。

3.2.3 券碼兌換

站外營銷券的發放方式與其他券不同,通過券碼進行兌換。券碼由後臺匯出,通過簡訊或者活動的方式發放到使用者,使用者根據券碼兌換後獲取相應的券。券碼的組成有一定的規則,在規則的基礎上要保證安全性,這種安全性主要是券碼校驗的準確性,防止已兌換券碼的再次兌換和無效券碼的惡意兌換。

3.3 精細化營銷能力設計

通過標籤組合配置的方式,優惠券提供精細化營銷的能力,以實現優惠券的千人千面。標籤可分為準實時和實時,值得注意的是,一些實時的標籤的處理需要前提條件,比如地區屬性需要使用者授權。

優惠券的精準觸達:

3.4 券和商品之間的關係

優惠券的使用需要和商品關聯,可關聯所有商品,也可以關聯部分商品。為了靈活性地滿足運營對於券關聯商品的配置,優惠券系統有兩種關聯方式:

a. 黑名單。

可用商品 = 全部商品 - 黑名單商品。

黑名單適用於券的可使用商品範圍比較廣這種情況,全部商品排除掉黑名單商品就是券的可使用範圍。

b. 白名單。

可用商品 = 白名單商品。

白名單適用於券的可使用商品範圍比較小這種情況,直接配置券的可使用商品。

除此以外,還有超級黑名單的配置,黑名單和白名單隻對單個券有效,超級黑名單對所有券有效。當前優惠券系統提供商品級的關聯,後續優惠券會支援商品分類維度的關聯,分類維度 + 商品維度可以更靈活地關聯優惠券和商品。

3.5 高效能保證

優惠券對接系統多,存在高流量場景,優惠券對外提供介面需保證高效能和高穩定性。

多級快取

為了提升查詢速度,減輕資料庫的壓力,同時為了應對瞬時高流量帶來熱點key的場景(比如釋出會直播結束切換流量至特定商品商詳頁、熱點活動商品商詳頁都會給優惠券系統帶來瞬時高流量),優惠券採用了多級快取的方式。

資料庫讀寫分離

優惠券除了上述所說的分庫分表外,在此基礎上還做了讀寫分離操作。主庫負責執行資料更新請求,然後將資料變更實時同步到所有從庫,用從庫來分擔查詢請求,解決資料庫寫入影響查詢的問題。主從同步存在延遲,正常情況下延遲不超過1ms,優惠券的領取或狀態變更存在一個耗時的過程,主從延遲對於使用者來說無感知。

依賴外部介面隔離熔斷

優惠券內部依賴了第三方的系統,為了防止因為依賴方服務不可用,產生連鎖效應,最終導致優惠券服務雪崩的事情發生,優惠券對依賴外部介面做了隔離和熔斷。

使用者維度優惠券欄位冗餘

查詢使用者相關的優惠券資料是優惠券最頻繁的查詢操作之一,使用者優惠券資料做了分庫分表,在查詢時無法關聯券規則表進行查詢,為了減少IO次數,使用者優惠券表中冗餘了部分券規則的欄位。優惠券規則表欄位較多,冗餘的欄位不能很多,要在效能和欄位數之間做好平衡。

四、總結及展望

最後對優惠券系統進行一個總結:

  • 不停機遷移,平穩過渡。自獨立後已穩定執行2年,效能足以支撐vivo商城未來3-5年的高速發展。

  • 系統解耦,迭代效率大幅提升。

  • 針對業務問題,原則是選擇合適實用的方案。

  • 具備完善的優惠券業務能力。

展望:目前優惠券系統主要服務於vivo商城,未來我們希望將優惠券能力開放,為內部其他業務方提供通用一體化的優惠券平臺。

作者:vivo網際網路開發團隊-Yan Chao

相關文章