Java+MySQL實(shí)現(xiàn)設(shè)計(jì)優(yōu)惠券系統(tǒng)
1 Scenario 場(chǎng)景
電商系統(tǒng)的促銷手段(Electronic Commerce Systems):
- 優(yōu)惠券
- 拼團(tuán)
- 砍價(jià)
- 老帶新
優(yōu)惠券的種類:
- 滿減券
- 直減券
- 折扣券
優(yōu)惠券系統(tǒng)的核心流程:
發(fā)券
發(fā)券的方式: 同步發(fā)送 or 異步發(fā)送
領(lǐng)券
- 誰(shuí)能領(lǐng)?
- 所有用戶 or 指定的用戶
- 領(lǐng)取上限
- 一個(gè)優(yōu)惠券最多能領(lǐng)取多少?gòu)垼?/li>
- 領(lǐng)取方式
- 用戶主動(dòng)領(lǐng)取 or 自動(dòng)發(fā)放被動(dòng)領(lǐng)取
用券
- 作用范圍
- 商品、商戶、類目
- 計(jì)算方式
- 是否互斥、是否達(dá)到門檻等
需求拆解
商家側(cè):
- 創(chuàng)建優(yōu)惠券
- 發(fā)送優(yōu)惠券
用戶側(cè):
- 領(lǐng)取優(yōu)惠券
- 下單
- 使用優(yōu)惠券
- 支付
2 Service 服務(wù)
2.1 服務(wù)結(jié)構(gòu)設(shè)計(jì)
2.2 優(yōu)惠券系統(tǒng)難點(diǎn)
券的分布式事務(wù),使用券的過程會(huì)出現(xiàn)的分布式問題分析
如何防止超發(fā)
如何大批量給用戶發(fā)券
如何限制券的使用條件
如何防止用戶重復(fù)領(lǐng)券
3 Storage存儲(chǔ)
模型的設(shè)計(jì)
優(yōu)惠券系統(tǒng) Coupon System 模型定義
優(yōu)惠券系統(tǒng)的難點(diǎn)
3.1 表單設(shè)計(jì)
券批次(券模板),coupon_batch
指一批優(yōu)惠券的抽象、模板,包含優(yōu)惠券的大部分屬性。
如商家創(chuàng)建了一批優(yōu)惠券,共1000張,使用時(shí)間為2022-11-11 00:00:00 ~ 2022-11-11 23:59:59,規(guī)定只有數(shù)碼類目商品才能使用,滿100減50。
券
發(fā)放到用戶的一個(gè)實(shí)體,已與用戶綁定。
如將某批次的優(yōu)惠券中的一張發(fā)送給某個(gè)用戶,此時(shí)優(yōu)惠券屬于用戶。
規(guī)則
優(yōu)惠券的使用有規(guī)則和條件限制,比如滿100減50券,需要達(dá)到門檻金額100元才能使用。
券批次表 coupon_batch
規(guī)則表 rule:
規(guī)則內(nèi)容:
{ threshold: 5.01 // 使用門檻 amount: 5 // 優(yōu)惠金額 use_range: 3 // 使用范圍,0—全場(chǎng),1—商家,2—類別,3—商品 commodity_id: 10 // 商品 id receive_count: 1 // 每個(gè)用戶可以領(lǐng)取的數(shù)量 is_mutex: true // 是否互斥,true 表示互斥,false 表示不互斥 receive_started_at: 2020-11-1 00:08:00 // 領(lǐng)取開始時(shí)間 receive_ended_at: 2020-11-6 00:08:00 // 領(lǐng)取結(jié)束時(shí)間 use_started_at: 2020-11-1 00:00:00 // 使用開始時(shí)間 use_ended_at: 2020-11-11 11:59:59 // 使用結(jié)束時(shí)間 }
優(yōu)惠券表 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-凍結(jié)', order_id varchar(255) null comment '對(duì)應(yīng)訂單ID', received_time datetime null comment '領(lǐng)取時(shí)間', validat_time datetime null comment '有效日期', used_time datetime null comment '使用時(shí)間' );
3.2 優(yōu)惠券系統(tǒng)
建券:
1、新建規(guī)則
INSERT INTO rule (name, type, rule_content) VALUES(“滿減規(guī)則”, 0, '{ threshold: 100 amount: 10 ...... }');
2、新建優(yōu)惠券批次
INSERT INTO coupon_batch (coupon_name, rule_id, total_count ) VALUES(“勞斯萊斯5元代金券”, 1010, 10000);
發(fā)券:
如何給大量用戶發(fā)券?
異步發(fā)送
觸達(dá)系統(tǒng)
- 短信、郵件
- 可通過調(diào)用第三方接口的方式實(shí)現(xiàn)
- 站內(nèi)信
- 通過數(shù)據(jù)庫(kù)插入記錄來(lái)實(shí)現(xiàn)
信息表 message
create table t_message ( id int null comment '信息ID', send_id int null comment '發(fā)送者id', rec_id int null comment '接受者id', content vachar(255) comment '站內(nèi)信內(nèi)容', is_read int null comment '是否已讀', send_time datetime comment '發(fā)送時(shí)間' ) comment '信息表';
先考慮用戶量很少的情況,商家要給所有人發(fā)站內(nèi)信,則先遍歷用戶表,再按照用戶表中的所有用戶依次將站內(nèi)信插入到 message 表中。這樣,如果有100個(gè)用戶,則群發(fā)一條站內(nèi)信要執(zhí)行100個(gè)插入操作。
系統(tǒng)用戶數(shù)增加到萬(wàn)級(jí)
發(fā)一條站內(nèi)信,就得重復(fù)插入上萬(wàn)條數(shù)據(jù)。而且這上萬(wàn)條數(shù)據(jù)的 content 一樣!假設(shè)一條站內(nèi)信占100K,發(fā)一次站內(nèi)信就要消耗十幾M。對(duì)此,可將原來(lái)的表拆成兩個(gè)表:
信息表 message
信息內(nèi)容表 message_content
發(fā)一封站內(nèi)信的步驟
- 往 message_content 插入站內(nèi)信的內(nèi)容
- 在 message 表中,給所有用戶插入一條記錄,標(biāo)識(shí)有一封站內(nèi)信
千w級(jí)用戶數(shù)
這就有【非活躍用戶】的問題,假設(shè)注冊(cè)用戶一千萬(wàn),根據(jù)二八原則,其中活躍用戶占20%。若采用上面拆成兩個(gè)表的情況,發(fā)一封“站內(nèi)信”,得執(zhí)行一千萬(wàn)個(gè)插入操作。可能剩下80%用戶基本都不會(huì)再登錄,其實(shí)只需對(duì)其中20%用戶插入數(shù)據(jù)。
信息表 message:
create table t_message ( id int null comment '信息 ID', # send_id int null comment '發(fā)送者 id', 去除該字段 rec_id int null comment '接受者 id', message_id int null comment '外鍵,信息內(nèi)容', is_read int null comment '是否已讀' ) comment '信息表';
create table t_message_content ( id int null comment '信息內(nèi)容id', send_id int null comment '發(fā)送者id', content varchar(255) null comment '內(nèi)容', send_time datetime null comment '發(fā)送時(shí)間' );
用戶側(cè)操作
登錄后,首先查詢 message_content 中的那些沒有在 message 中有記錄的數(shù)據(jù),表示是未讀的站內(nèi)信。在查閱站內(nèi)信的內(nèi)容時(shí),再將相關(guān)的記錄插入 message。
系統(tǒng)側(cè)操作
發(fā)站內(nèi)信時(shí):
- 只在 message_content 插入站內(nèi)信的主體內(nèi)容
- message 不插入記錄
假設(shè)商家要給 10W 用戶發(fā)券:
有什么問題?重復(fù)消費(fèi),導(dǎo)致超發(fā)!
- 運(yùn)營(yíng)提供滿足條件的用戶文件,上傳到發(fā)券管理后臺(tái)并選擇要發(fā)送的優(yōu)惠券
- 管理服務(wù)器根據(jù)【用戶ID】、【券批次ID】生成消息,發(fā)送到MQ
- 優(yōu)惠券服務(wù)器消費(fèi)消息
# 記住使用事務(wù)哦! 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;
領(lǐng)券
步驟:
- 校驗(yàn)優(yōu)惠券余量
SELECT total_count FROM coupon_batch WHERE batch_id = 1111;
- 新增優(yōu)惠券用戶表,扣減余量
# 注意事務(wù)! 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;
用戶領(lǐng)券過程中,其實(shí)也會(huì)出現(xiàn)類似秒殺場(chǎng)景。秒殺場(chǎng)景下會(huì)有哪些問題,如何解決?
解決用戶重復(fù)領(lǐng)取或多領(lǐng):
Redis 數(shù)據(jù)校驗(yàn)!
- 領(lǐng)券前,先查緩存
# 判斷成員元素是否是集合的成員 SISMEMBER KEY VALUE SISMEMBER batch_id:1111:user_id 1001
- 領(lǐng)券
- 領(lǐng)券后,更新緩存
# 將一或多個(gè)成員元素加入到集合中,已經(jīng)存在于集合的成員元素將被忽略 SADD KEY VALUE1......VALUEN SADD batch_id:1111:user_id 1001
用券
何時(shí)校驗(yàn)優(yōu)惠券使用規(guī)則?
- 確認(rèn)訂單(√)
- 提交訂單
- 立即付款
確認(rèn)訂單頁(yè),對(duì)優(yōu)惠券進(jìn)行校驗(yàn):
- 判斷是否過期
- 判斷適用范圍
- 判斷是否達(dá)到門檻
- 判斷是否互斥
返回可用券
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; 復(fù)制代碼
選擇可用券,并返回結(jié)果
同時(shí)操作多個(gè)服務(wù),如何保證一致性?
表設(shè)計(jì)
優(yōu)惠券操作記錄表 Coupon_opt_record
create table t_coupon_opt_record ( user_id int null comment '用戶id', coupon_id int null comment '優(yōu)惠券id', operating int null comment '操作,0-鎖定、1-核銷、2-解鎖', operated_at datetime null comment '操作時(shí)間' );
TCC,Try-Confirm-Cancel,目前分布式事務(wù)主流解決方案。
階段一:Try
對(duì)資源進(jìn)行凍結(jié),預(yù)留業(yè)務(wù)資源
創(chuàng)建訂單時(shí),將優(yōu)惠券狀態(tài)改為 “凍結(jié)”
階段二:Confirm
確認(rèn)執(zhí)行業(yè)務(wù)操作,做真正提交,將第一步Try中凍結(jié)的資源,真正扣減
訂單支付成功,將優(yōu)惠券狀態(tài)改為 “已使用”
階段三:Cancel
取消執(zhí)行業(yè)務(wù)操作,取消Try階段預(yù)留的業(yè)務(wù)資源
支付失敗/超時(shí)或訂單關(guān)閉情況,將優(yōu)惠券狀態(tài)改為 “未使用”
Scale擴(kuò)展
快過期券提醒:
定時(shí)掃券表:
缺點(diǎn):掃描數(shù)據(jù)量太大,隨著歷史數(shù)據(jù)越來(lái)越多,會(huì)影響線上主業(yè)務(wù),最終導(dǎo)致慢SQL。
延時(shí)消息:
缺點(diǎn):有些券的有效時(shí)間太長(zhǎng)了(30天)以上,有可能造成大量 MQ 積壓
新增通知表:
優(yōu)點(diǎn):掃描的數(shù)據(jù)量小,效率高。刪除無(wú)用的已通知的數(shù)據(jù)記錄
通知信息表(notify_msg)設(shè)計(jì)
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 '需要執(zhí)行通知的日期', notify_type int null comment '通知類型,1-過期提醒', notif_time timestamp null comment '通知的時(shí)間,在該時(shí)間戳所在天內(nèi)通知', status int null comment '通知狀態(tài),0-初始狀態(tài)、1-成功、2-失敗', constraint t_notify_msg_id_uindex unique (id) ); alter table t_notify_msg add primary key (id);
過期券提醒:
- 在創(chuàng)建優(yōu)惠券的時(shí)候就將需要提醒的記錄插入提醒表中notify_msg
- 把用戶ID+批次ID+通知日期作為唯一索引,防止同一個(gè)批次有重復(fù)的記錄通知,保證每天只會(huì)被通知一次
- 建立notify_time,通知時(shí)間索引,每日的通知掃描通過該索引列查詢,通過索引列來(lái)提高查詢效率
- 通知完成后該表中的數(shù)據(jù)變失去了意義,通過定時(shí)任務(wù)將該數(shù)據(jù)刪除
數(shù)據(jù)庫(kù)層面優(yōu)化 - 索引
發(fā)券接口,限流保護(hù)
前端限流:
點(diǎn)擊一次后,按鈕短時(shí)間內(nèi)置灰
后端限流:
部分請(qǐng)求直接跳轉(zhuǎn)到【繁忙頁(yè)】
到此這篇關(guān)于Java+MySQL實(shí)現(xiàn)設(shè)計(jì)優(yōu)惠券系統(tǒng)的文章就介紹到這了,更多相關(guān)Java+MySQ設(shè)計(jì)系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Spring Boot 2.0.2+Ajax解決跨域請(qǐng)求的問題
這篇文章主要介紹了詳解Spring Boot 2.0.2+Ajax解決跨域請(qǐng)求的問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03java 枚舉類定義靜態(tài)valueOf(java.lang.String)方法的問題及解決
這篇文章主要介紹了java 枚舉類定義靜態(tài)valueOf(java.lang.String)方法的問題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09idea中怎樣創(chuàng)建并運(yùn)行第一個(gè)java程序
這篇文章主要介紹了idea中怎樣創(chuàng)建并運(yùn)行第一個(gè)java程序問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08SpringBoot攔截器實(shí)現(xiàn)登錄攔截的示例代碼
本文主要介紹了SpringBoot攔截器實(shí)現(xiàn)登錄攔截,文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03java ArrayList和Vector的區(qū)別詳解
這篇文章主要介紹了java ArrayList和Vector的區(qū)別詳解的相關(guān)資料,并附簡(jiǎn)單實(shí)例代碼,需要的朋友可以參考下2016-11-11