1 Scenario 場景
電商系統的促銷手段(Electronic Commerce Systems):
- 優惠券
- 拼團
- 砍價
- 老帶新
優惠券的種類
- 滿減券
- 直減券
- 折扣券
優惠券系統的核心流程
發券
發券的方式:同步發送 or 異步發送
領券
- 誰能領?
- 所有用戶 or 指定的用戶
- 領取上限
- 一個優惠券最多能領取多少張?
- 領取方式
- 用戶主動領取 or 自動發放被動領取
用券
- 作用范圍
- 商品、商戶、類目
- 計算方式
- 是否互斥、是否達到門檻等
需求拆解
商家側:
- 創建優惠券
- 發送優惠券
用戶側:
- 領取優惠券
- 下單
- 使用優惠券
- 支付
2 Service 服務
2.1 服務結構設計
2.2 優惠券系統難點
券的分布式事務,使用券的過程會出現的分布式問題分析
如何防止超發
如何大批量給用戶發券
如何限制券的使用條件
如何防止用戶重復領券
3 Storage存儲
模型的設計
優惠券系統 Coupon System 模型定義
優惠券系統的難點
3.1 表單設計
券批次(券模板),coupon_batch
指一批優惠券的抽象、模板,包含優惠券的大部分屬性。
如商家創建了一批優惠券,共1000張,使用時間為2022-11-11 00:00:00 ~ 2022-11-11 23:59:59,規定只有數碼類目商品才能使用,滿100減50。
券
發放到用戶的一個實體,已與用戶綁定。
如將某批次的優惠券中的一張發送給某個用戶,此時優惠券屬于用戶。
規則
優惠券的使用有規則和條件限制,比如滿100減50券,需要達到門檻金額100元才能使用。
券批次表 coupon_batch
規則表 rule:
規則內容:
{
threshold: 5.01 // 使用門檻
amount: 5 // 優惠金額
use_range: 3 // 使用范圍,0—全場,1—商家,2—類別,3—商品
commodity_id: 10 // 商品 id
receive_count: 1 // 每個用戶可以領取的數量
is_mutex: true // 是否互斥,true 表示互斥,false 表示不互斥
receive_started_at: 2020-11-1 00:08:00 // 領取開始時間
receive_ended_at: 2020-11-6 00:08:00 // 領取結束時間
use_started_at: 2020-11-1 00:00:00 // 使用開始時間
use_ended_at: 2020-11-11 11:59:59 // 使用結束時間
}
優惠券表 coupon:
create table t_coupon
(
coupon_id int null comment '券ID,主鍵',
user_id int null comment '用戶ID',
batch_id int null comment '批次ID',
status int null comment '0-未使用、1-已使用、2-已過期、3-凍結',
order_id varchar(255) null comment '對應訂單ID',
received_time datetime null comment '領取時間',
validat_time datetime null comment '有效日期',
used_time datetime null comment '使用時間'
);
優惠券系統
建券
1、新建規則
INSERT INTO rule (name, type, rule_content)
VALUES(“滿減規則”, 0, '{
threshold: 100
amount: 10
......
}');
2、新建優惠券批次
INSERT INTO coupon_batch (coupon_name, rule_id, total_count )
VALUES(“勞斯萊斯5元代金券”, 1010, 10000);
發券
如何給大量用戶發券?
異步發送
觸達系統
- 短信、郵件
- 可通過調用第三方接口的方式實現
- 站內信
- 通過數據庫插入記錄來實現
信息表 message
create table t_message
(
id int null comment '信息ID',
send_id int null comment '發送者id',
rec_id int null comment '接受者id',
content vachar(255) comment '站內信內容',
is_read int null comment '是否已讀',
send_time datetime comment '發送時間'
)
comment '信息表';
先考慮用戶量很少的情況,商家要給所有人發站內信,則先遍歷用戶表,再按照用戶表中的所有用戶依次將站內信插入到 message 表中。這樣,如果有100個用戶,則群發一條站內信要執行100個插入操作。
系統用戶數增加到萬級
發一條站內信,就得重復插入上萬條數據。而且這上萬條數據的 content 一樣!假設一條站內信占100K,發一次站內信就要消耗十幾M。對此,可將原來的表拆成兩個表:
信息表 message
信息內容表 message_content
發一封站內信的步驟
- 往 message_content 插入站內信的內容
- 在 message 表中,給所有用戶插入一條記錄,標識有一封站內信
千w級用戶數
這就有【非活躍用戶】的問題,假設注冊用戶一千萬,根據二八原則,其中活躍用戶占20%。若采用上面拆成兩個表的情況,發一封“站內信”,得執行一千萬個插入操作。可能剩下80%用戶基本都不會再登錄,其實只需對其中20%用戶插入數據。
信息表 message:
create table t_message
(
id int null comment '信息 ID',
# send_id int null comment '發送者 id', 去除該字段
rec_id int null comment '接受者 id',
message_id int null comment '外鍵,信息內容',
is_read int null comment '是否已讀'
)
comment '信息表';
create table t_message_content
(
id int null comment '信息內容id',
send_id int null comment '發送者id',
content varchar(255) null comment '內容',
send_time datetime null comment '發送時間'
);
用戶側操作
登錄后,首先查詢 message_content 中的那些沒有在 message 中有記錄的數據,表示是未讀的站內信。在查閱站內信的內容時,再將相關的記錄插入 message。
系統側操作
發站內信時:
- 只在 message_content 插入站內信的主體內容
- message 不插入記錄
假設商家要給 10W 用戶發券
有什么問題?重復消費,導致超發!
- 運營提供滿足條件的用戶文件,上傳到發券管理后臺并選擇要發送的優惠券
- 管理服務器根據【用戶ID】、【券批次ID】生成消息,發送到MQ
- 優惠券服務器消費消息
# 記住使用事務哦!
INSERT INTO coupon (user_id, coupon_id,batch_id)
VALUES(1001, 66889, 1111);
UPDATE coupon_batch SET total_count = total_count - 1,
assign_count = assign_count + 1
WHERE batch_id = 1111 AND total_count > 0;
領券
步驟
- 校驗優惠券余量
SELECT total_count FROM coupon_batch
WHERE batch_id = 1111;
- 新增優惠券用戶表,扣減余量
# 注意事務!
INSERT INTO coupon (user_id, coupon_id,batch_id)
VALUES(1001, 66889, 1111);
UPDATE coupon_batch SET total_count = total_count - 1,
assign_count = assign_count + 1
WHERE batch_id = 1111 AND total_count > 0;
用戶領券過程中,其實也會出現類似秒殺場景。秒殺場景下會有哪些問題,如何解決?
解決用戶重復領取或多領
redis 數據校驗!
- 領券前,先查緩存
# 判斷成員元素是否是集合的成員
SISMEMBER KEY VALUE
SISMEMBER batch_id:1111:user_id 1001
- 領券
- 領券后,更新緩存
# 將一或多個成員元素加入到集合中,已經存在于集合的成員元素將被忽略
SADD KEY VALUE1......VALUEN
SADD batch_id:1111:user_id 1001
用券
何時校驗優惠券使用規則?
- 確認訂單(√)
- 提交訂單
- 立即付款
確認訂單頁,對優惠券進行校驗:
- 判斷是否過期
- 判斷適用范圍
- 判斷是否達到門檻
- 判斷是否互斥
返回可用券
SELECT batch_id FROM coupon WHERE user_id = 1001 AND status = 0;
SELECT rule_id FROM coupon_batch WHERE batch_id = 1111;
SELECT name, type, rule_content FROM rule WHERE rule_id = 1010;
選擇可用券,并返回結果
同時操作多個服務,如何保證一致性?
表設計
優惠券操作記錄表 Coupon_opt_record
create table t_coupon_opt_record
(
user_id int null comment '用戶id',
coupon_id int null comment '優惠券id',
operating int null comment '操作,0-鎖定、1-核銷、2-解鎖',
operated_at datetime null comment '操作時間'
);
TCC,Try-Confirm-Cancel,目前分布式事務主流解決方案。
- 階段一:Try
對資源進行凍結,預留業務資源
創建訂單時,將優惠券狀態改為 “凍結”
- 階段二:Confirm
確認執行業務操作,做真正提交,將第一步Try中凍結的資源,真正扣減
訂單支付成功,將優惠券狀態改為 “已使用”
- 階段三:Cancel
取消執行業務操作,取消Try階段預留的業務資源
支付失敗/超時或訂單關閉情況,將優惠券狀態改為 “未使用”
Scale擴展
快過期券提醒
定時掃券表
缺點:掃描數據量太大,隨著歷史數據越來越多,會影響線上主業務,最終導致慢SQL。
延時消息
缺點:有些券的有效時間太長了(30天)以上,有可能造成大量 MQ 積壓
新增通知表
優點:掃描的數據量小,效率高。刪除無用的已通知的數據記錄
通知信息表(notify_msg)設計
create table t_notify_msg
(
id bigint auto_increment comment '自增主鍵',
coupon_id bigint null comment '券id',
user_id bigint null comment '用戶id',
notify_day varchar(255) null comment '需要執行通知的日期',
notify_type int null comment '通知類型,1-過期提醒',
notif_time timestamp null comment '通知的時間,在該時間戳所在天內通知',
status int null comment '通知狀態,0-初始狀態、1-成功、2-失敗',
constraint t_notify_msg_id_uindex
unique (id)
);
alter table t_notify_msg
add primary key (id);
過期券提醒:
- 在創建優惠券的時候就將需要提醒的記錄插入提醒表中notify_msg
- 把用戶ID+批次ID+通知日期作為唯一索引,防止同一個批次有重復的記錄通知,保證每天只會被通知一次
- 建立notify_time,通知時間索引,每日的通知掃描通過該索引列查詢,通過索引列來提高查詢效率
- 通知完成后該表中的數據變失去了意義,通過定時任務將該數據刪除
數據庫層面優化 - 索引
發券接口,限流保護
前端限流
點擊一次后,按鈕短時間內置灰
后端限流
部分請求直接跳轉到【繁忙頁】