萬級TPS優(yōu)惠券系統(tǒng)設計與實踐
共 4899字,需瀏覽 10分鐘
·
2024-11-13 08:45
??目錄
1 背景介紹
2 什么是優(yōu)惠券系統(tǒng)?
3 優(yōu)惠券創(chuàng)建
4 優(yōu)惠券派發(fā)
5 后續(xù)優(yōu)化
01
優(yōu)惠券是電商常見的營銷手段,是營銷平臺中的一個重要組成部分,騰訊云 MALL 也需要搭建優(yōu)惠券相關的平臺能力來更好的助力賦能商家的各種促銷場景。
02
這里找了幾個電商平臺的優(yōu)惠券相關頁面:
依次是某東、某寶、騰訊云 MALL ,這里各式各樣的優(yōu)惠券背后涉及的相關系統(tǒng),可以統(tǒng)稱為優(yōu)惠券系統(tǒng)。所以單說優(yōu)惠券系統(tǒng)是一個很龐大的系統(tǒng),這里收斂一下講其中主要有四大核心能力:創(chuàng)建、派發(fā)、使用、統(tǒng)計。
本篇主要介紹的是平臺如何創(chuàng)建和派發(fā)優(yōu)惠券到用戶賬戶的券包里,即上面提到的四大核心能力中的創(chuàng)建和派發(fā)。
03
先簡單了解一下兩個概念:優(yōu)惠券批次、優(yōu)惠券。
優(yōu)惠券批次:一批相同優(yōu)惠券的生成模版。
優(yōu)惠券:根據(jù)批次信息生成,優(yōu)惠券與批次的對應關系是 N:1。
批次 ID ;
優(yōu)惠券名稱;
優(yōu)惠券類型;
庫存數(shù)量;
優(yōu)惠規(guī)則如:滿減,滿折等;
生效規(guī)則:固定生效時間、領取后生效時間等;
領取規(guī)則:批次每天限領數(shù)量、用戶每天限領數(shù)量、用戶總限領數(shù)量等;
使用規(guī)則:指定商家、指定商品、指定類目、指定場景等。
優(yōu)惠券 ID:分布式 ID 全局唯一;
批次 ID;
用戶 ID;
優(yōu)惠券狀態(tài);
上下文信息。
批次表的數(shù)據(jù)寫入主要是 B 端后臺管理來操作,這里不多贅述。
優(yōu)惠券表數(shù)據(jù)主要通過派發(fā)動作與用戶關聯(lián)后寫入,后面會展開介紹。
04
庫存管理,如何防止超發(fā),保障庫存安全。
場景復雜,如何支持高并發(fā)及瞬時高流量毛刺場景。
流量毛刺示意:
庫存扣減;
生成優(yōu)惠券。
直接用數(shù)據(jù)庫做庫存管理,面臨問題:高并發(fā)導致數(shù)據(jù)庫崩潰、性能瓶頸明顯。
緩存做庫存管理:數(shù)據(jù)不一致、穿透、擊穿、雪崩等問題。
最終方案:
Redis+Lua+庫存異步分段增補:
Redis+Lua:支持高并發(fā)庫存扣減。
庫存異步分段增補:支持高并發(fā)的前提下靈活分配庫存。
Lua 腳本示意(示意代碼僅供學習參考):
--批次的HashKeylocal stockKey = KEYS[1];--Argv 參數(shù)local stockId = ARGV[1];local couponId = ARGV[2];local uid = ARGV[3];--該批次當天最大發(fā)放量local maxByDay = ARGV[4];-- 每人限領local maxByUser = ARGV[5];--當前時間Strlocal crtDateStr = ARGV[6];-- 每人每日限領local dailyMaxByUser = ARGV[7];stockId = tonumber(stockId);maxByUser = tonumber(maxByUser);maxByDay = tonumber(maxByDay);dailyMaxByUser = tonumber(dailyMaxByUser);--StockKey nilif not stockKey thenreturn '-4'end--Argv nilif not stockId or not couponId or not uid or not maxByUser or not maxByDay or not crtDateStr or not dailyMaxByUser thenreturn '-5'endlocal leftAmountField = 'left_amount';local res = redis.call("HMGET", stockKey, leftAmountField, crtDateStr);local leftAmount = tonumber(res[1]);local crtDispatchAmount = tonumber(res[2]);local couponIdSetKey = stockKey .. ':coupon:zset';--優(yōu)惠券ID是否已經(jīng)分配庫存local score = redis.call("ZSCORE", couponIdSetKey, couponId);-- couponId 已經(jīng)存在if score thenreturn '-6';end-- 庫存不足if not leftAmount or leftAmount <= 0 thenreturn '-3';end--達到當天發(fā)放上限if crtDispatchAmount and crtDispatchAmount >= maxByDay thenreturn '-1';end-- 該批次每人每日領取數(shù)量HashKeylocal dailyUserAcquireNumKey = stockKey .. ":user:acquire:" .. crtDateStr;if dailyMaxByUser > 0 thenlocal dailyUserAcquireNum = redis.call("HGET", dailyUserAcquireNumKey, uid);dailyUserAcquireNum = tonumber(dailyUserAcquireNum);-- 達到每人每日領取上限if dailyUserAcquireNum and dailyUserAcquireNum >= dailyMaxByUser thenreturn '-7'endend--該批次用戶領取數(shù)量HashKeylocal userAcquireNumKey = stockKey .. ":user:acquire";local usrAcquireNum = redis.call("HGET", userAcquireNumKey, uid);usrAcquireNum = tonumber(usrAcquireNum);--達到用戶領取上限if usrAcquireNum and usrAcquireNum >= maxByUser thenreturn '-2'end--扣減庫存-1local leftAmountAfterOp = redis.call("HINCRBY", stockKey, leftAmountField, -1);--當天發(fā)放量+1local crtDispatchAmountAfterOp = redis.call("HINCRBY", stockKey, crtDateStr, 1);--當前用戶發(fā)放量+1local usrAcquireNumAfterOp = redis.call("HINCRBY", userAcquireNumKey, uid, 1);-- 當前用戶當天發(fā)放量+1local dailyUserAcquireNumAfterOp = redis.call("HINCRBY", dailyUserAcquireNumKey, uid, 1);redis.call("ZADD", couponIdSetKey, uid, couponId);--返回操作之后的上下文,緩存中剩余量,當天已經(jīng)發(fā)放量,用戶已經(jīng)領取量,用戶當天已經(jīng)領取量return '0|' .. leftAmountAfterOp .. '|' .. crtDispatchAmountAfterOp .. '|' .. usrAcquireNumAfterOp .. '|' .. dailyUserAcquireNumAfterOp
分段增補示意:
介紹:
每當 Redis 剩余庫存小于 M 個時,異步從數(shù)據(jù)庫增補 N 個庫存到 Redis 里,保證 Redis 庫存數(shù)量一直小于等于數(shù)據(jù)庫。
屏蔽流量直接打到數(shù)據(jù)庫,減輕數(shù)據(jù)庫壓力。
Redis+數(shù)據(jù)庫控制,雙重保證不超發(fā)。
庫存增補的 M 和 N 可以根據(jù)實際業(yè)務需要靈活調(diào)配。
M 可以理解為業(yè)務發(fā)券速率兜底。比如:發(fā)快補慢提示無庫存等。
N 可以理解為極端情況下最大允許丟失的庫存數(shù)量。
主流程如圖:
扣減庫存成功同步生成優(yōu)惠券信息寫入數(shù)據(jù)庫,同樣會面臨高并發(fā)導致數(shù)據(jù)庫崩潰的問題,系統(tǒng)瓶頸明顯不可取。
這里再加緩存的話,解決緩存問題會讓業(yè)務變得更復雜,結(jié)合第二個主要問題:瞬時高流量毛刺。
最終方案:
庫存扣減成功后異步生成優(yōu)惠券,達到整體流程支持高并發(fā),且可以解決流量毛刺的問題。PS:分布式事務問題。
結(jié)合自身業(yè)務場景,對比權衡了多種分布式事務解決方案,最終選用本地事務表+最大努力通知來解決分布式事務問題。
介紹:
通過消息異步生成優(yōu)惠券落庫處理來支持高并發(fā),引入一張本地事務表達成數(shù)據(jù)的最終一致性。
主流程如圖:
數(shù)據(jù)參考:
結(jié)合自身實際業(yè)務測試環(huán)境壓測目標 1W/TPS 示意(系統(tǒng)整體支持橫向擴容進一步提升性能)。
示意:
05
回顧整體方案,同批次場景仍存在熱點問題,針對這里可以做一些優(yōu)化來提升系統(tǒng)性能,如:資源分桶,聚合扣減,熱點更新技術等。如何解決熱點問題?下面結(jié)合發(fā)券場景列舉幾種方案做一下對比介紹,可供參考。
熱點示意:
簡介:同一個批次的庫存分成多份,通過分散庫存扣減請求提升性能。
優(yōu)勢:水平擴展能力強。
重點關注:
分桶 Key 路由傾斜問題,理想情況是所有 Key 平均對應分桶。
各桶之間庫存傾斜與性能權衡的問題,理想情況是所有分桶消耗速率一致。
簡介:聚合相同批次的請求統(tǒng)一扣減,通過聚合請求量來提升服務整體性能。
優(yōu)勢:前置聚合請求利于提高服務穩(wěn)定性。
重點關注:
聚合策略的設計需要在系統(tǒng)穩(wěn)定性和性能上做取舍。
臨界庫存如何與聚合策略適配的問題。
簡介:熱點更新技術詳細介紹見騰訊云文檔:
https://cloud.tencent.com/document/product/237/13402
優(yōu)勢:適用數(shù)據(jù)庫鎖層面的熱點優(yōu)化。
重點關注:
依賴數(shù)據(jù)庫適用場景較單一。
小結(jié):每種方案的實現(xiàn)均有利有弊,最后都需要在系統(tǒng)性能和復雜度上做權衡取舍,最終選出契合自身實際業(yè)務的才是最好的方案。
????歡迎加入騰訊云開發(fā)者社群,享前沿資訊、大咖干貨,找興趣搭子,交同城好友,更有鵝廠招聘機會、限量周邊好禮等你來~
(長按圖片立即掃碼)
