1. Preface
Seckill is essentially a short-term, sudden and high concurrent access problem. Its business features are as follows:
- Triggered by timing, the flow suddenly increases in an instant
- Only some of the requests for seckill are successful
- Seckill products are often limited in quantity, not oversold, but can accept less
- 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
- 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
- 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
- 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.
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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.