RocketMQ practice -- high concurrency seckill scenario

Keywords: Apache JSON less Database

1. Preface

Seckill is essentially a short-term, sudden and high concurrent access problem. Its business features are as follows:

  1. Triggered by timing, the flow suddenly increases in an instant
  2. Only some of the requests for seckill are successful
  3. Seckill products are often limited in quantity, not oversold, but can accept less
  4. Do not require immediate return of real order results

This article focuses on the practical use of RocketMQ in seckill scenario, and does not explain other business processes of seckill in detail.

The following is the flow chart of seckill:

For specific implementation, see detailed code: Big guy source code

2. Overview of seckill business

By asynchronizing the core business process of seckill, we can divide the main process into two stages: receipt and order.

2.1 seckill process - receipt

  1. The user accesses the seckill entry, submits the seckill request to the seckill platform billing gateway, and the platform performs pre verification on the seckill request
  2. After the verification is passed, the order request will be submitted through the middle layer such as cache / queue / thread pool, and at the same time when the delivery is completed, it will return "in queue" to the user
  3. For the order request whose pre verification fails, the synchronization returns the seckill order failure

At this point, the interaction with the user side is over.

During the receipt process, put the seckill order into the RocketMQ middle tier.

2.2 seckill process - order

In the process of placing an order, the pressure of the platform through the buffer of the middle layer is actually much smaller. On the one hand, some illegal requests are filtered out during the synchronous verification process of user placing an order; on the other hand, we do some flow limiting, filtering and other logic operations on the middle layer to speed limit, pressure and other operations on the order request, and slowly digest the order request in the interior To minimize the impact of traffic on the persistence layer of the platform. In fact, it reflects the characteristics of "peak cutting and valley filling" in the middle layer.

Based on the above premise, we briefly summarize the business logic of the single part of seckill.

  1. Seckill order service obtains the order request of the middle layer, and performs the real pre order verification. Here, it mainly performs the real inventory verification
  2. After deducting Inventory (or locking inventory) successfully, the real order operation is initiated. Deduct Inventory (lock inventory) and order operation are generally in one transaction field
  3. After the order is placed successfully, the platform will often launch a message push to inform the user that the order is placed successfully and guide the user to pay
  4. If the user fails to pay for a period of time (e.g. 30mins), the order will be voided and the inventory will be restored, providing other users in the queue with purchase opportunities
  5. If the payment is successful, the order status will be updated and the order will flow to other subsystems, such as the logistics system for subsequent processing such as shipment of the order in the process of successful payment

This is basically the core main process of seckill business.

Further abstract the scenario of seckill request - > middle tier - > real order, is it very similar to the asynchronous business processing mode we often use?

I believe that you can see that, yes, this is the "producer consumer" model.

In the process, producer consumer mode is often realized by blocking queue or waiting notification mechanism, and between services, it is often realized by message queue, which is also the technical implementation method used in this actual combat. In this paper, through RocketMQ message queue, seckill order is decoupled to cut peak and fill valley and improve system throughput.

Next, I will explain how to use RocketMQ to implement the above scenarios.

3. actual combat

3.1 structure

  1. Users can visit seckill gateway service to launch a seckill operation on the products they are interested in. In particular, for product information, it has been loaded into seckill gateway service during system initialization. During the pre inventory verification, the user's order flow has been filtered once according to the cache
  2. After the gateway has fully pre verified the seckill order, it delivers the seckill order message to RocketMQ and returns it to the user in the queue synchronously
  3. Seckill order service subscribes to the message of seckill order, processes the message idempotent, and conducts the real order operation after the real verification of the commodity inventory

3.2 database structure

3.3 NameServer configuration

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MQNamesrvConfig {

    @Value("${rocketmq.nameServer.offline}")
    String offlineNamesrv;

    @Value("${rocketmq.nameServer.aliyun}")
    String aliyunNamesrv;

    /**
     * Select the nameServer address according to the environment
     * @return
     */
    public String nameSrvAddr() {
        String envType = System.getProperty("envType");
        //System.out.println(envType);
        if (StringUtils.isBlank(envType)) {
            throw new IllegalArgumentException("please insert envType");
        }
        switch (envType) {
            case "offline" : {
                return offlineNamesrv;
            }
            case "aliyun" : {
                return aliyunNamesrv;
            }
            default : {
                throw new IllegalArgumentException("please insert right envType, offline/aliyun");
            }
        }
    }
}

3.4 message protocol

Here, we implement the template methods encode and decode of BaseMsg (to encode and decode the message respectively), and realize the self encoding and decoding of the message protocol by setting the properties of this object.

/**
 * @desc Basic protocol class
 */
public abstract class BaseMsg {

    public Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**Version number, default 1.0*/
    private String version = "1.0";
    /**topic name*/
    private String topicName;

    public abstract String encode();

    public abstract void decode(String msg);

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getTopicName() {
        return topicName;
    }

    public void setTopicName(String topicName) {
        this.topicName = topicName;
    }

    @Override
    public String toString() {
        return "BaseMsg{" +
                "version='" + version + '\'' +
                ", topicName='" + topicName + '\'' +
                '}';
    }
}
/**
 * @className OrderNofityProtocol
 * @desc Order result notification agreement
 */
public class ChargeOrderMsgProtocol extends BaseMsg implements Serializable {

    private static final long serialVersionUID = 73717163386598209L;

    /**Order number*/
    private String orderId;
    /**User's order mobile number*/
    private String userPhoneNo;
    /**Commodity id*/
    private String prodId;
    /**User transaction amount*/
    private String chargeMoney;

    private Map<String, String> header;
    private Map<String, String> body;

    @Override
    public String encode() {
        // Assemble message protocol header
        ImmutableMap.Builder headerBuilder = new ImmutableMap.Builder<String, String>()
                .put("version", this.getVersion())
                .put("topicName", MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getTopic());
        header = headerBuilder.build();

        body = new ImmutableMap.Builder<String, String>()
                .put("orderId", this.getOrderId())
                .put("userPhoneNo", this.getUserPhoneNo())
                .put("prodId", this.getProdId())
                .put("chargeMoney", this.getChargeMoney())
                .build();

        ImmutableMap<String, Object> map = new ImmutableMap.Builder<String, Object>()
                .put("header", header)
                .put("body", body)
                .build();

        // Return serialized message Json string
        String ret_string = null;
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            ret_string = objectMapper.writeValueAsString(map);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("ChargeOrderMsgProtocol Message serialization json abnormal", e);
        }
        return ret_string;
    }

    @Override
    public void decode(String msg) {
        Preconditions.checkNotNull(msg);
        ObjectMapper mapper = new ObjectMapper();
        try {
            JsonNode root = mapper.readTree(msg);
            // header
            this.setVersion(root.get("header").get("version").asText());
            this.setTopicName(root.get("header").get("topicName").asText());
            // body
            this.setOrderId(root.get("body").get("orderId").asText());
            this.setUserPhoneNo(root.get("body").get("userPhoneNo").asText());
            this.setChargeMoney(root.get("body").get("chargeMoney").asText());
            this.setProdId(root.get("body").get("prodId").asText());
        } catch (IOException e) {
            throw new RuntimeException("ChargeOrderMsgProtocol Message deserialization exception", e);
        }
    }

    public String getOrderId() {
        return orderId;
    }

    public ChargeOrderMsgProtocol setOrderId(String orderId) {
        this.orderId = orderId;
        return this;
    }

    public String getUserPhoneNo() {
        return userPhoneNo;
    }

    public ChargeOrderMsgProtocol setUserPhoneNo(String userPhoneNo) {
        this.userPhoneNo = userPhoneNo;
        return this;
    }

    public String getProdId() {
        return prodId;
    }

    public ChargeOrderMsgProtocol setProdId(String prodId) {
        this.prodId = prodId;
        return this;
    }

    public String getChargeMoney() {
        return chargeMoney;
    }

    public ChargeOrderMsgProtocol setChargeMoney(String chargeMoney) {
        this.chargeMoney = chargeMoney;
        return this;
    }

    @Override
    public String toString() {
        return "ChargeOrderMsgProtocol{" +
                "orderId='" + orderId + '\'' +
                ", userPhoneNo='" + userPhoneNo + '\'' +
                ", prodId='" + prodId + '\'' +
                ", chargeMoney='" + chargeMoney + '\'' +
                ", header=" + header +
                ", body=" + body +
                "} " + super.toString();
    }
}

3.5 seckill order producer initialization

Load by @ PostConstruct (i.e. init())

import org.apache.rocketmq.acl.common.AclClientRPCHook;
import org.apache.rocketmq.acl.common.SessionCredentials;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.gateway.common.config.MQNamesrvConfig;
import org.apache.rocketmq.gateway.common.util.LogExceptionWapper;
import org.apache.rocketmq.message.constant.MessageProtocolConst;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

/**
 * @className SecKillChargeOrderProducer
 * @desc Seckill order producer initialization
 */
@Component
public class SecKillChargeOrderProducer {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecKillChargeOrderProducer.class);

    @Autowired
    MQNamesrvConfig namesrvConfig;

    @Value("${rocketmq.acl.accesskey}")
    String aclAccessKey;

    @Value("${rocketmq.acl.accessSecret}")
    String aclAccessSecret;


    private DefaultMQProducer defaultMQProducer;

    @PostConstruct
    public void init() {
        defaultMQProducer =
                new DefaultMQProducer
                        (MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getProducerGroup(),
                                new AclClientRPCHook(new SessionCredentials(aclAccessKey, aclAccessSecret)));
        defaultMQProducer.setNamesrvAddr(namesrvConfig.nameSrvAddr());
        // Send failed retries
        defaultMQProducer.setRetryTimesWhenSendFailed(3);
        try {
            defaultMQProducer.start();
        } catch (MQClientException e) {
            LOGGER.error("[Seckill order producer]--SecKillChargeOrderProducer Loading exception!e={}", LogExceptionWapper.getStackTrace(e));
            throw new RuntimeException("[Seckill order producer]--SecKillChargeOrderProducer Loading exception!", e);
        }
        LOGGER.info("[Seckill order producer]--SecKillChargeOrderProducer Load complete!");
    }

    public DefaultMQProducer getProducer() {
        return defaultMQProducer;
    }
}

3.6 second kill order team (producer)

/**
* Platform single interface
* @param chargeOrderRequest
* @return
*/
@RequestMapping(value = "charge.do", method = {RequestMethod.POST})
public @ResponseBody Result chargeOrder(@ModelAttribute ChargeOrderRequest chargeOrderRequest) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String sessionId = attributes.getSessionId();
// Pre order parameter verification
if (!secKillChargeService.checkParamsBeforeSecKillCharge(chargeOrderRequest, sessionId)) {
return Result.error(CodeMsg.PARAM_INVALID);
}
// Pre product verification
String prodId = chargeOrderRequest.getProdId();
if (!secKillChargeService.checkProdConfigBeforeKillCharge(prodId, sessionId)) {
return Result.error(CodeMsg.PRODUCT_NOT_EXIST);
}
// Pre reduced inventory
if (!secKillProductConfig.preReduceProdStock(prodId)) {
return Result.error(CodeMsg.PRODUCT_STOCK_NOT_ENOUGH);
}
// Second kill order team
return secKillChargeService.secKillOrderEnqueue(chargeOrderRequest, sessionId);
}

Producer: seckillchargeservice:: seckillorenqueue

/**
 * Second kill order team
 * @param chargeOrderRequest
 * @param sessionId
 * @return
 */
@Override
public Result secKillOrderEnqueue(ChargeOrderRequest chargeOrderRequest, String sessionId) {

    // Order number generation, assembly seckill order message protocol
    String orderId = UUID.randomUUID().toString();
    String phoneNo = chargeOrderRequest.getUserPhoneNum();
	
    //Message encapsulation
    ChargeOrderMsgProtocol msgProtocol = new ChargeOrderMsgProtocol();
    msgProtocol.setUserPhoneNo(phoneNo)
        .setProdId(chargeOrderRequest.getProdId())
        .setChargeMoney(chargeOrderRequest.getChargePrice())
        .setOrderId(orderId);
    String msgBody = msgProtocol.encode();
    LOGGER.info("Second kill order team,Message protocol={}", msgBody);

    DefaultMQProducer mqProducer = secKillChargeOrderProducer.getProducer();
    // Assemble RocketMQ message body
    Message message = new Message(MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getTopic(), msgBody.getBytes());
    try {
        // message sending
        SendResult sendResult = mqProducer.send(message);
        //Judge SendStatus
        if (sendResult == null) {
            LOGGER.error("sessionId={},Seckill order message delivery failed,Order failure.msgBody={},sendResult=null", sessionId, msgBody);
            return Result.error(CodeMsg.BIZ_ERROR);
        }
        if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
            LOGGER.error("sessionId={},Seckill order message delivery failed,Order failure.msgBody={},sendResult=null", sessionId, msgBody);
            return Result.error(CodeMsg.BIZ_ERROR);
        }
        ChargeOrderResponse chargeOrderResponse = new ChargeOrderResponse();
        BeanUtils.copyProperties(msgProtocol, chargeOrderResponse);
        LOGGER.info("sessionId={},Seckill order message delivered successfully,Order entry.Ginseng production chargeOrderResponse={},sendResult={}", sessionId, chargeOrderResponse.toString(), JSON.toJSONString(sendResult));
        return Result.success(CodeMsg.ORDER_INLINE, chargeOrderResponse);
    } catch (Exception e) {
        int sendRetryTimes = mqProducer.getRetryTimesWhenSendFailed();
        LOGGER.error("sessionId={},sendRetryTimes={},Seckill order message delivery exception,Order failure.msgBody={},e={}", sessionId, sendRetryTimes, msgBody, LogExceptionWapper.getStackTrace(e));
    }
    return Result.error(CodeMsg.BIZ_ERROR);
}

3.7 seckill consumption

3.7.1 define consumer client

Second kill order consumer

@Component
public class SecKillChargeOrderConsumer {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecKillChargeOrderConsumer.class);

    @Autowired
    MQNamesrvConfig namesrvConfig;

    @Value("${rocketmq.acl.accesskey}")
    String aclAccessKey;

    @Value("${rocketmq.acl.accessSecret}")
    String aclAccessSecret;

    private DefaultMQPushConsumer defaultMQPushConsumer;

    @Resource(name = "secKillChargeOrderListenerImpl")
    private MessageListenerConcurrently messageListener;

    @PostConstruct
    public void init() {
        defaultMQPushConsumer =
                new DefaultMQPushConsumer(
                    MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getConsumerGroup(),
                        new AclClientRPCHook(new SessionCredentials(aclAccessKey, aclAccessSecret)),
                        // Average allocation queue algorithm, hash
                        new AllocateMessageQueueAveragely());
        defaultMQPushConsumer.setNamesrvAddr(namesrvConfig.nameSrvAddr());
        // Consumption from the beginning
        defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // Consumption mode: cluster mode
        // Cluster: the same message will only be consumed by one consumer node
        // Broadcast: every consumer will consume the same message
        defaultMQPushConsumer.setMessageModel(MessageModel.CLUSTERING);
        // Register listener
        defaultMQPushConsumer.registerMessageListener(messageListener);
        // Set the number of messages pulled each time. The default value is 1
        defaultMQPushConsumer.setConsumeMessageBatchMaxSize(1);
        // Subscribe to all messages
        try {
            defaultMQPushConsumer.subscribe(MessageProtocolConst.SECKILL_CHARGE_ORDER_TOPIC.getTopic(), "*");
            // Launch consumer
            defaultMQPushConsumer.start();
        } catch (MQClientException e) {
            LOGGER.error("[Second kill order consumer]--SecKillChargeOrderConsumer Loading exception!e={}", LogExceptionWapper.getStackTrace(e));
            throw new RuntimeException("[Second kill order consumer]--SecKillChargeOrderConsumer Loading exception!", e);
        }
        LOGGER.info("[Second kill order consumer]--SecKillChargeOrderConsumer Load complete!");
    }
}

3.7.2 realize the core logic of seckill receipt

Realize the logic of seckill receipt core, that is, to realize our own messagelistener concurrency.

@Component
public class SecKillChargeOrderListenerImpl implements MessageListenerConcurrently {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecKillChargeOrderListenerImpl.class);

    @Resource(name = "secKillOrderService")
    SecKillOrderService secKillOrderService;

    @Autowired
    SecKillProductService secKillProductService;

    /**
     * Seckill core consumption logic
     * @param msgs
     * @param context
     * @return
     */
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        try {
            for (MessageExt msg : msgs) {
                // Message decoding
                String message = new String(msg.getBody());
                int reconsumeTimes = msg.getReconsumeTimes();
                String msgId = msg.getMsgId();
                String logSuffix = ",msgId=" + msgId + ",reconsumeTimes=" + reconsumeTimes;
                LOGGER.info("[Second kill order consumer]-SecKillChargeOrderConsumer-Message received,message={},{}", message, logSuffix);

                // Deserialization protocol entity
                ChargeOrderMsgProtocol chargeOrderMsgProtocol = new ChargeOrderMsgProtocol();
                chargeOrderMsgProtocol.decode(message);
                LOGGER.info("[Second kill order consumer]-SecKillChargeOrderConsumer-Deserialize to seckill receipt Order entity chargeOrderMsgProtocol={},{}", chargeOrderMsgProtocol.toString(), logSuffix);

                // Consumption idempotent: query whether the order corresponding to orderId already exists
                String orderId = chargeOrderMsgProtocol.getOrderId();
                OrderInfoDobj orderInfoDobj = secKillOrderService.queryOrderInfoById(orderId);
                if (orderInfoDobj != null) {
                    LOGGER.info("[Second kill order consumer]-SecKillChargeOrderConsumer-The current order has been received,No need for repeat consumption!,orderId={},{}", orderId, logSuffix);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }

                // Business idempotent: only one seckill order for the same prodId + the same userPhoneNo
                OrderInfoDO orderInfoDO = new OrderInfoDO();
                orderInfoDO.setProdId(chargeOrderMsgProtocol.getProdId())
                        .setUserPhoneNo(chargeOrderMsgProtocol.getUserPhoneNo());
                Result result = secKillOrderService.queryOrder(orderInfoDO);
                if (result != null && result.getCode().equals(CodeMsg.SUCCESS.getCode())) {
                    LOGGER.info("[Second kill order consumer]-SecKillChargeOrderConsumer-Current user={},Seckill products={}Order already exists,Do not repeat second kill,orderId={}",
                            orderInfoDO.getUserPhoneNo(), orderInfoDO.getProdId(), orderId);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }

                // Seckill order warehousing
                OrderInfoDO orderInfoDODB = new OrderInfoDO();
                BeanUtils.copyProperties(chargeOrderMsgProtocol, orderInfoDODB);

                // Stock check
                String prodId = chargeOrderMsgProtocol.getProdId();
                SecKillProductDobj productDobj = secKillProductService.querySecKillProductByProdId(prodId);
                // Take inventory verification
                int currentProdStock = productDobj.getProdStock();
                if (currentProdStock <= 0) {
                    LOGGER.info("[decreaseProdStock]The current product is sold out,Message consumption successful!prodId={},currStock={}", prodId, currentProdStock);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                // Official order
                if (secKillOrderService.chargeSecKillOrder(orderInfoDODB)) {
                    LOGGER.info("[Second kill order consumer]-SecKillChargeOrderConsumer-Seckill order warehousing succeeded,Message consumption successful!,Warehousing entity orderInfoDO={},{}", orderInfoDO.toString(), logSuffix);
                    // Simulate order processing, directly modify the order status to being processed
                    secKillOrderService.updateOrderStatusDealing(orderInfoDODB);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        } catch (Exception e) {
            LOGGER.info("[Second kill order consumer]Consumption abnormality,e={}", LogExceptionWapper.getStackTrace(e));
        }
        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
    }
}

3.7.3 seckill actual storage

The actual order operation and the actual inventory deduction are in the same local transaction

/**
 * Seckill order warehousing
 * @param orderInfoDO
 * @return
 */
@Transactional(rollbackFor = Exception.class)
@Override
public boolean chargeSecKillOrder(OrderInfoDO orderInfoDO) {
    int insertCount = 0;
    String orderId = orderInfoDO.getOrderId();
    String prodId = orderInfoDO.getProdId();

    // Reduce stock
    if (!secKillProductService.decreaseProdStock(prodId)) {
        LOGGER.info("[insertSecKillOrder]orderId={},prodId={},Inventory reduction before order placement failed,Order failure!", orderId, prodId);
        // TODO can send a notice to the user to inform seckill that the order failed. Reason: the goods have been sold out
        return false;
    }
    // Set product name
    SecKillProductDobj productInfo = secKillProductService.querySecKillProductByProdId(prodId);
    orderInfoDO.setProdName(productInfo.getProdName());
    try {
        insertCount = secKillOrderMapper.insertSecKillOrder(orderInfoDO);
    } catch (Exception e) {
        LOGGER.error("[insertSecKillOrder]orderId={},Seckill order warehousing[abnormal],Transaction rollback,e={}", orderId, LogExceptionWapper.getStackTrace(e));
        String message =
                String.format("[insertSecKillOrder]orderId=%s,Seckill order warehousing[abnormal],Transaction rollback", orderId);
        throw new RuntimeException(message);
    }
    if (insertCount != 1) {
        LOGGER.error("[insertSecKillOrder]orderId={},Seckill order warehousing[fail],Transaction rollback,e={}", orderId);
        String message =
                String.format("[insertSecKillOrder]orderId=%s,Seckill order warehousing[fail],Transaction rollback", orderId);
        throw new RuntimeException(message);
    }
    return true;
}

4. Summary & References

Summary

Look at the flow chart again. If you don't understand it, look at the source code.

For payment, logistics and other operations after placing an order, RocketMQ can be used for asynchronous processing.

The whole code of this article comes from Big guy source code

Only for learning and studying RocketMQ practice.

Reference material

38 original articles published, 27 praised, 30000 visitors+
Private letter follow

Posted by fatepower on Sun, 02 Feb 2020 22:06:54 -0800