實戰分佈式之電商高併發秒殺收單核心要點及代碼實現

說罷秒殺網關相關的核心要點,咱們接着聊聊秒殺收單相關的核心要點與代碼實現。數據庫

本文重點說明如下幾點:緩存

  1. 業務場景概述
  2. 經過消息隊列異步收單
  3. 實際庫存扣減
  4. 實際下單操做

業務場景概述

首先對業務場景進行概述。bash

完整的業務流可參考實戰分佈式之電商高併發秒殺場景總覽併發

秒殺收單核心業務邏輯以下:app

  1. 秒殺下單消費者從MQ中獲取到下單消息,開始下單操做
  2. 首先進行下單前的消息冪等校驗,對於已經存在的下單消息不予消費
  3. 接着進行真實的庫存判斷,若是庫存不夠扣減則再也不消費,這裏應當經過消息推送告知用戶商品已售罄,提示用戶下次再來
  4. 若是庫存足夠,則扣減庫存並下單。這二者在同一個本地事務域中,保證扣減完庫存必定可以下單成功
  5. 下單成功後,經過消息推送通知用戶對秒殺訂單進行付款,付款後進行後續的發貨等操做

經過消息隊列異步收單

接着講解下如何經過消息隊列進行異步收單。異步

關於如何對消息進行封裝,能夠參考 實戰分佈式之電商高併發秒殺網關核心要點及代碼實現 , 這裏再也不贅述。分佈式

定義消費者客戶端

咱們須要定義一個進行下單操做的消費者客戶端,並對秒殺收單消息進行訂閱。ide

@PostConstruct
public void init() {
    defaultMQPushConsumer = new DefaultMQPushConsumer(MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getConsumerGroup());
    defaultMQPushConsumer.setNamesrvAddr(nameSrvAddr);
    // 從頭開始消費
    defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
    // 消費模式:集羣模式
    defaultMQPushConsumer.setMessageModel(MessageModel.CLUSTERING);
    // 註冊監聽器
    defaultMQPushConsumer.registerMessageListener(messageListener);
    // 訂閱全部消息
    try {
        defaultMQPushConsumer.subscribe(MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getTopic(), "*");
        // 啓動消費者
        defaultMQPushConsumer.start();
    } catch (MQClientException e) {
        LOGGER.error("[秒殺下單消費者]--SecKillChargeOrderConsumer加載異常!e={}", LogExceptionWapper.getStackTrace(e));
        throw new RuntimeException("[秒殺下單消費者]--SecKillChargeOrderConsumer加載異常!", e);
    }
    LOGGER.info("[秒殺下單消費者]--SecKillChargeOrderConsumer加載完成!");
}複製代碼

接着須要實現秒殺收單核心的邏輯,也就是實現咱們本身的MessageListenerConcurrently。高併發

@Component
public class SecKillChargeOrderListenerImpl implements MessageListenerConcurrently {

    /**
    * 秒殺核心消費邏輯
    * @param msgs
    * @param context
    * @return
    */
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {複製代碼

定義一個類實現接口MessageListenerConcurrently,經過@Component標註爲一個SpringBean。ui

這就是秒殺客戶端的主要骨架代碼。

進行庫存校驗

咱們雖然在秒殺網關已經對庫存進行了校驗,但那是不可靠的。

緣由在於秒殺網關的庫存是在啓動時預熱的,後續扣減是基於緩存進行的。核心目的在於減小對數據庫的壓力。

當下單消費者進行真實下單的時候就須要對庫存進行校驗,此時數據庫的壓力已經很小了,由於在網關的前置校驗已經對流量進行了大幅度的削弱。

咱們看一下真實庫存校驗邏輯

// 庫存校驗
String prodId = chargeOrderMsgProtocol.getProdId();
SecKillProductDobj productDobj = secKillProductService.querySecKillProductByProdId(prodId);
// 取庫存校驗
int currentProdStock = productDobj.getProdStock();
if (currentProdStock <= 0) {
    LOGGER.info("[decreaseProdStock]當前商品已售罄,消息消費成功!prodId={},currStock={}", prodId, currentProdStock);
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}複製代碼

此處先對庫存進行了查詢並校驗是否已經小於等於0,若是等於0說明已經售罄,則再也不進行消費。

實際下單操做

實際下單操做與實際庫存扣減處於同一個本地事務中,核心代碼邏輯以下:

// 減庫存
if (!secKillProductService.decreaseProdStock(prodId)) {
    LOGGER.info("[insertSecKillOrder]orderId={},prodId={},下單前減庫存失敗,下單失敗!", orderId, prodId);
    // TODO 此處可給用戶發送通知,告知秒殺下單失敗,緣由:商品已售罄
    return false;
}
// 設置產品名稱
SecKillProductDobj productInfo = secKillProductService.querySecKillProductByProdId(prodId);
orderInfoDO.setProdName(productInfo.getProdName());
try {
    insertCount = secKillOrderMapper.insertSecKillOrder(orderInfoDO);
} catch (Exception e) {
    LOGGER.error("[insertSecKillOrder]orderId={},秒殺訂單入庫[異常],事務回滾,e={}", orderId, LogExceptionWapper.getStackTrace(e));
    String message =
            String.format("[insertSecKillOrder]orderId=%s,秒殺訂單入庫[異常],事務回滾", orderId);
    throw new RuntimeException(message);
}
if (insertCount != 1) {
    LOGGER.error("[insertSecKillOrder]orderId={},秒殺訂單入庫[失敗],事務回滾,e={}", orderId);
    String message =
            String.format("[insertSecKillOrder]orderId=%s,秒殺訂單入庫[失敗],事務回滾", orderId);
    throw new RuntimeException(message);
}
return true;複製代碼

咱們首先進行減庫存操做,若是減庫存失敗,則事務回滾。

若是扣減成功則進行下單操做,下單操做失敗則事務回滾,同時對庫存進行回滾。此處經過Spring的聲明式事務對事務進行處理。使用默認事務傳播級別 Propagation.REQUIRED 便可。

小結

本文主要對高併發秒殺場景下的收單部分的重點邏輯進行了講解,對庫存真實扣減、真實下單部分的邏輯和注意點以代碼的形式作了較爲直觀的展示。

到此,實戰分佈式之電商高併發秒殺場景的系列就暫時告一段落。

實際上,在收單以後還有支付、物流相關的操做,它們都可以經過使用RocketMQ之類的消息引擎進行異步化處理。思路和秒殺下單主邏輯很類似,礙於篇幅就暫時不作講解了,感興趣的同窗能夠參考以前的思路本身實現,聰明的你必定可以觸類旁通。更多的實戰相關的總結與分享

相關文章
相關標籤/搜索