促銷系統之秒殺功能模塊


訂單所涉及到的後臺系統包括系統模塊,網關模塊,調度器模塊,商品模塊,促銷模塊,訂單模塊
庫存系統(無-商品配額)、物流系統(無-到店取貨)、風控系統(無)等。
訂單業務的流轉主要依靠完善的後臺系統

促銷系統

我負責的是秒殺模塊和網關認證授權功能,秒殺模塊其實是促銷模塊的一個子功能,由於我們的項目老開發人員比較急,爲了優先實現功能,多個功能模塊只是多模塊開發,模塊之間調用需要通過httpClient,並沒有引入RPC框架;而且各模塊的數據庫的表也比較混亂沒有分庫,功能邏輯耦合度也比較大,而且併發能力也比較弱只能達到幾百,我們做秒殺功能模塊其實是爲了後期把服務都遷移到新框架中。
我這次負責的是促銷系統的秒殺功能,因爲要抗擊高併發,所以單獨創建獨立微服務,獨立數據庫。大佬把舊項目各個模塊的數據庫也進行了分庫,然後做了微服務適配。我創建秒殺模塊創建成獨立服務,引入微服務網關進行認證和授權。我對系統安全方面和應對高併發方面比較瞭解。

http://www.woshipm.com/pd/916015.html

  1. 促銷系統主要包括:活動管理和促銷類型管理
    活動信息:活動名稱,活動時間,活動規則,活動商品
    活動狀態:未開始的活動:隨意編輯或刪除活動
    開始的活動:有終止活動操作,一般不允許修改相關的商品
    已結束的活動:不可編輯
  2. 促銷類型:滿減、滿贈、滿折、加價購、特價、套餐、預售、秒殺等。

風控系統比較簡單,是通過設定業務規則,限定一個用戶只能參加一個活動,而且只能搶購有限數量的商品

在這裏插入圖片描述

普通訂單流程-區別於秒殺訂單流程

在這裏插入圖片描述

前端訂單系統

  1. 訂單信息
    用戶信息:聯通手機號
    商品信息:規格,價格,數量
    活動信息,優惠金額
    支付信息:訂單總金額,實付金額,優惠金額,支付單號,訂單號
  2. 訂單狀態
    待付款,付款成功,退款中,退款失敗,交易成功(),交易關閉(取消訂單)

後臺訂單系統

後臺訂單系統和前端訂單系統展示的信息相對應,包括訂單列表以及訂單詳情的展示。

  1. 訂單列表:查詢所有用戶所有的下單記錄,主要是一些核心信息-訂單編號,下單時間,下單用戶,商品信息,實付金額,訂單狀態,維權狀態
  2. 訂單詳情:點擊某個訂單查他的詳情,分爲三部分-訂單信息,支付信息,門店信息
    訂單信息:商品名稱、規格、ID,商品單價、購買數量、實付金額
    支付信息: 商品總額、運費、優惠金額、實付金額、支付時間、支付單號、交易單號
    門店信息:門店名稱,門店地址,門店聯繫電話

秒殺訂單模塊需要解決的問題

  1. 高併發引起的緩存雪崩,緩存擊穿,緩存穿透
  2. 超賣,商家預估賣100個可以賺點還可以營銷,結果多賣出200個,不發貨用戶投訴,平臺封店,發貨就血虧,怎麼辦。
  3. 惡意請求。驗證碼,後臺邏輯效驗等
  4. 鏈接暴露。在秒殺前置灰,加密url
  5. 把其他服務打掛。每秒上萬請求直接打到數據庫把庫打掛,如果秒殺服務還涉及到其他業務,並且沒做降級、限流、熔斷啥的,會把其他服務一起打掛

針對秒殺活動對症下藥

服務單一原則

我們系統是微服務架構,分佈式部署,我們可以將秒殺也單獨做一個服務。
單獨給秒殺服務創建一個秒殺庫:秒殺訂單,秒殺商品
單一職責的好處就是就算秒殺沒抗住,秒殺庫崩了,服務掛了,也不會影響到其他服務。

按鈕控制

秒殺前按鈕都是置灰的,秒殺時間到了才能點擊。
按鈕點擊之後也給它置灰幾秒,防止重複點擊。
驗證碼登陸驗證。

庫存預熱

秒殺的本質,就是對庫存的搶奪。通過redis預扣庫存的實現,避免了到底是支付減庫存、下單減庫存兩種方案的選擇,因爲其各有利弊。而且還將驗庫存和減庫存通過mq進行了解耦,極大的提升系統併發度和響應時間。使得系統從qps不到1000提升到上萬的能力。

每個秒殺用戶都去數據庫查詢庫存效驗庫存,然後扣減庫存,所有的操作都在數據庫,會導致數據庫頂不住

秒殺前通過定時任務將庫存加載到redis中去,讓效驗庫存的操作在redis中進行,然後發mq同步redis和數據庫的數據。
在高併發場景下,多個線程同時效驗該商品庫存滿足條件,然後同時扣減庫存導致超賣問題發生。
Lua腳本功能時Redis2.6版本最大的亮點,通過對Lua腳本的支持,Redis解決了長久以來不能高效處理CAS命令的缺點,並且可以通過組合多個redis命令,輕鬆實現以前很難實現或者不能高效實現的模式。

Lua腳本實現了redis事務操作,我們將判斷扣減庫存的操作 和 預減庫存的操作寫到一個Lua腳本,返回扣減後的庫存數量,如果庫存數量<0則判斷爲發生超賣,將之前扣減的數目再加回去;如果庫存數量>0,則發一個訂單消息,然後訂單模塊生成訂單,庫存模塊扣減庫存,支付模塊進行支付;支付成功後發送一個支付成功消息,然後優惠券模塊扣減優惠券,積分模塊增加積分,最後向用戶發送短信通知。

JSONObject.parseObject(data);
庫存=配額
key=手機號/微信ID+活動ID  value=訂單數據
kafka的key=redis的key  value=orderId

通過Lua腳本實現搶紅包功能,很優秀

Lua腳本示例

在這裏插入圖片描述

## 嘗試獲得紅包,如果成功,則返回json字符串,如果不成功,則返回空
## eval函數(腳本名稱,參數個數,keys1,keys2,keys3,keys4)
## keys1:預生成的紅包隊列 keys2: 已消費的紅包隊列 keys3: 去重map keys4:用戶id
 static String tryGetHongBaoScript =   
        "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n"  //如果用戶已經搶過紅包,則返回nill
        + "return nil\n"  
        + "else\n"  
        + "local hongBao = redis.call('rpop', KEYS[1]);\n"    //取出一個小紅包
        + "if hongBao then\n"  
        + "local x = cjson.decode(hongBao);\n"   
        + "x['userId'] = KEYS[4];\n"                          //加入用戶信息
        + "local re = cjson.encode(x);\n"  
        + "redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n"  //將用戶放到去重的set中去,防止多次搶紅包
        + "redis.call('lpush', KEYS[2], re);\n"               //將紅包放入以消費隊列中
        + "return re;\n"  
        + "end\n"  
        + "end\n"  
        + "return nil";  

static public void testTryGetHongBao() throws InterruptedException {  
    final CountDownLatch latch = new CountDownLatch(threadCount);  
    
    long startTime = System.currentTimeMillis();
    System.err.println("start:" + startTime);  
    
    for(int i = 0; i < threadCount; ++i) {  
        final int temp = i;  
        Thread thread = new Thread() {  
            public void run() {  
                Jedis jedis = new Jedis(host, port);  
                String sha = jedis.scriptLoad(tryGetHongBaoScript);  
                int j = honBaoCount/threadCount * temp;  
                while(true) {  
                    //搶紅包方法
                    Object object = jedis.eval(tryGetHongBaoScript, 4, 
                            hongBaoList/*預生成的紅包隊列*/, 
                            hongBaoConsumedList, /*已經消費的紅包隊列*/
                            hongBaoConsumedMap, /*去重的map*/
                            "" + j  /*用戶id*/
                            );  
                    j++;  
                    if (object != null) {  
                        //do something...
  //                          System.out.println("get hongBao:" + object);  
                    }else {  
                        //已經取完了  
                        if(jedis.llen(hongBaoList) == 0)  
                            break;  
                    }  
                }  
                latch.countDown();  
            }  
        };  
        thread.start();  
    }  

訂單生成、庫存扣減與支付邏輯

扣減庫存的三種方案

剛開始我們使用的是付款減庫存,防止惡意買家大量下單。
微信支付回調會返回微信生成的訂單號以及我們自己生成的訂單號

a) 設置的秒殺活動的庫存,總是莫名其妙的減少了。分析發現是因爲我們把減庫存放在微信支付的成功回調裏面的,而微信會回調這個url8次,導致多次減庫存。最後我們通過接口冪等進行了解決
b) 用戶下單顯示的不是最新的數據庫,支付時用戶經常由於庫存不足而支付失敗,這導致用戶體驗十分不好。

  1. 下單減庫存
    優點:實時減庫存,避免付款時因庫存不足減庫存的問題
    缺點:惡意買家大量下單將庫存用完,但是不付款,導致真正想買的人買不到
  2. 付款減庫存
    優點:防止惡意買家大量下單
    缺點:下單顯示的庫存可能不是最新的庫存數 ,支付時出現支付失敗。多次回調問題。用戶體驗問題。
  3. 預扣庫存(普通訂單採用的方式)
    下單顯示最新的庫存,下單後保留這個庫存一段時間(鎖定庫存),超過保留時間庫存釋放。超過再支付則支付失敗
    優點:下單實時減庫存,緩解惡意買家大量下單的問題,保留時間內未支付則釋放庫存
    缺點:保留時間內,惡意買家大量下單將庫存用完。
    解決: 每個用戶只能參加一個活動,購買一個商品。登陸驗證碼。
    1、創建訂單,狀態標記未支付,鎖定庫存
    2、支付完成後,標記訂單狀態完成,並減去庫存
    3、支付超時,訂單標記關閉,並釋放庫存
    4、訂單取消,訂單標記關閉,並釋放庫存

如何解決惡意買家大量下單問題

  1. 限制用戶下單數量
  2. 標識惡意買家58

如何解決下單成功而支付失敗的問題

備用庫存:商品庫用完之後,如果還有用戶支付,則直接扣減備用庫
優點:緩解部門用戶支付失敗問題
缺點:不能從根本解決問題,若併發量很大,還是會出現大量用戶下單成功缺庫存不足而支付失敗的問題

支付成功多次回調:把減庫存放在微信支付成功回調url裏面

微信支付成功後,微信支付平臺會發送8次回調地址,這樣就得做接口冪等

秒殺鏈接加鹽

鏈接要是提前暴露出去可能有人直接訪問url就提前秒殺了
剛開始我們想到做個秒殺開始、截止時間,但是這種方案解決不了通過程序進行搶購。
將URL動態化,通過MD5之類的加密隨機的字符串去做url,然後通過前端代碼獲取url後臺效驗才能通過

redis集羣

redis集羣,主從同步,讀寫分離。
開啓持久化保證高可用

Nginx

Nginx是高性能web服務器,併發輕鬆上萬併發,但普通Tomcat只能頂住幾百的併發
買流量機
幾萬併發——>Nginx——>Tomcat集羣

限流&降級&熔斷

庫存系統-可刪減,作爲商品的一個屬性

保持商品庫存數量的準確性是庫存系統最根本的功能
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章