首頁 > 軟體

Java+MySQL實現設計優惠券系統

2022-05-22 13:00:40

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 '使用時間'
 );

3.2 優惠券系統

建券:

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,通知時間索引,每日的通知掃描通過該索引列查詢,通過索引列來提高查詢效率
  • 通知完成後該表中的資料變失去了意義,通過定時任務將該資料刪除

資料庫層面優化 - 索引

發券介面,限流保護

前端限流:

點選一次後,按鈕短時間內建灰

後端限流:

部分請求直接跳轉到【繁忙頁】

到此這篇關於Java+MySQL實現設計優惠券系統的文章就介紹到這了,更多相關Java+MySQ設計系統內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com