Abstract:

In this article, we will use RabbitMQ dead-letter queues to deal with the problem of a RabbitMQ dead-letter queue being generated and not being paid.

Content:

For the messaging middleware RabbitMQ, Debug has been briefly shared in previous chapters and will not be covered here. In this article we will use RabbitMQ’s dead-letter queue to implement this requirement: “A user who has successfully executed a kill and created an order should be able to make a payment, but there is a case where the user is not able to make the payment.

For this scenario, you can experience it on some shopping mall platforms. After selecting the goods, add them to the shopping cart and click to settle, there will be a countdown to remind you that you need to complete the payment within the specified time, or the order will be invalid!

For this kind of business logic processing, the traditional approach is to use “timer mode”, periodic polling to obtain the order has exceeded the specified time, and then execute a series of processing measures (for example, try to send SMS to the user, remind the order will expire after how long…). In this second kill system we will invalidate the order with the RabbitMQ dead letter queue component!

“Dead letter queue”, unthinkably, is a special queue can delay, delay a certain time to process the message, it is relative to the “ordinary queue”, can achieve “into the dead letter queue message is not immediately processed, but can wait for a certain time to process” function! However, the ordinary queue is not good, that is, the message entering the queue will be immediately consumed by the corresponding consumer listener. The basic message model of the ordinary queue is shown in the figure below:



However, for the “dead letter queue”, its composition and use is relatively more complex. In normal circumstances, the dead letter queue consists of three core components: Dead-letter switch routing + + dead-letter TTL survival time ~ non-essential (news), and dead-letter queue can be caused by “basic switches for producers + basic routing” binding, so the producer is first send a message to the “basic switches + routing” binding with message model, which indirectly into the dead-letter queue, when TTL, The message will “hang up” and enter the message model bound to the next relay, the dead-letter switch + dead-letter route of the consumer below. As shown below:

Below, we use the actual code to build the message model of the dead letter queue, and apply this message model to the function module of the second kill system.

(1) First, you need to create a dead-letter queue message model in the RabbitmqConfig configuration class. The complete source code is shown below:

// Build a dead letter Queue message model @bean public Queue after seckill success - order timeout not paidsuccessKillDeadQueue(){
    Map<String, Object> argsMap= Maps.newHashMap();
    argsMap.put("x-dead-letter-exchange",env.getProperty("mq.kill.item.success.kill.dead.exchange"));
    argsMap.put("x-dead-letter-routing-key",env.getProperty("mq.kill.item.success.kill.dead.routing.key"));
    return new Queue(env.getProperty("mq.kill.item.success.kill.dead.queue"),true.false.false,argsMap); } // @bean public TopicExchangesuccessKillDeadProdExchange() {return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange"),true.false); } // create basic switch + basic route -> dead-letter queue Binding @bean public BindingsuccessKillDeadProdBinding() {return BindingBuilder.bind(successKillDeadQueue()).to(successKillDeadProdExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key")); } // The real Queue @bean public QueuesuccessKillRealQueue() {return new Queue(env.getProperty("mq.kill.item.success.kill.dead.real.queue"),true); } // @bean public TopicExchangesuccessKillDeadExchange() {return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.exchange"),true.false); } // Dead-letter switch + dead-letter route -> real queue Binding @bean public BindingsuccessKillDeadBinding() {return BindingBuilder.bind(successKillRealQueue()).to(successKillDeadExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.routing.key"));
}
Copy the code

The environment variable object instance env reads the variable configured in the application.properties configuration file as follows:

# Automatic invalidation of order timeout without payment - dead letter queue message model
mq.kill.item.success.kill.dead.queue=${mq.env}.kill.item.success.kill.dead.queue
mq.kill.item.success.kill.dead.exchange=${mq.env}.kill.item.success.kill.dead.exchange
mq.kill.item.success.kill.dead.routing.key=${mq.env}.kill.item.success.kill.dead.routing.key

mq.kill.item.success.kill.dead.real.queue=${mq.env}.kill.item.success.kill.dead.real.queue
mq.kill.item.success.kill.dead.prod.exchange=${mq.env}.kill.item.success.kill.dead.prod.exchange
mq.kill.item.success.kill.dead.prod.routing.key=${mq.env}.kill.item.success.kill.dead.prod.routing.key

The unit is ms
mq.kill.item.success.kill.expire=20000
Copy the code


(2) Following the successful creation of the message model, we need to develop the “send messages to a dead queue” function in the RabbitMQ general message sending service RabbitSenderService. In this function method, we specify the TTL of the message, which is the configured variable: Mq. Kill. Item. Success. Kill. The value of the expire, namely 20 s; The complete source code is as follows:

/ / SEC kill successfully generated after buying order - send information into the dead-letter queue and waiting for a certain time expiration timeout unpaid orders public void sendKillSuccessOrderExpireMsg (final String orderCode) {try {if (StringUtils.isNotBlank(orderCode)){
            KillSuccessUserInfo info=itemKillSuccessMapper.selectByCode(orderCode);
            if(info! =null){ rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); rabbitTemplate.setExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange"));
                rabbitTemplate.setRoutingKey(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key"));
                rabbitTemplate.convertAndSend(info, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { MessageProperties mp=message.getMessageProperties(); mp.setDeliveryMode(MessageDeliveryMode.PERSISTENT); mp.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,KillSuccessUserInfo.class); //TODO: set TTL(20s) mp.setexpiration (env.getProperty()"mq.kill.item.success.kill.expire"));
                        returnmessage; }}); } } }catch (Exception e){ log.error("Generate purchase order after seckill is successful - send message to dead letter queue, waiting for a certain period of time expired and not paid orders - exception occurs, message: {}",orderCode,e.fillInStackTrace()); }}Copy the code

From this “send message to dead letter queue” code, we can see that the message is first entered into the message model of the dead letter queue bound to “basic switch + basic route”! When the message to TTL, will naturally from the dead letter queue out (that is, “freed”), and then into the next relay station, that is: “dead letter switch + dead letter route” bound into the message model of the real queue, finally by the consumer listening consumption!

At this point, you can run the entire project and system on an external Tomcat server, then open the RabbitMQ back-end console application and find the dead letter queue. You can see the details of the dead letter queue, as shown below:


(3) Finally, we need to listen for the “real queue” message in RabbitMQ’s common message listening service, RabbitReceiverService: here we are invalidating the order (if it has not been paid!). , its complete source code is as follows:

- Queues = {RabbitListener(queues = {RabbitListener(queues = {RabbitListener);"${mq.kill.item.success.kill.dead.real.queue}"},containerFactory = "singleListenerContainer")
public void consumeExpireOrder(KillSuccessUserInfo info){
    try {
        log.info("User failed to pay after timeout - listener - received message :{}",info);

        if(info! =null){ ItemKillSuccess entity=itemKillSuccessMapper.selectByPrimaryKey(info.getCode());if(entity! =null && entity.getStatus().intValue()==0){ itemKillSuccessMapper.expireOrder(info.getCode()); } } }catch (Exception e){ log.error("User failed to pay after timeout - listener - exception occurred:",e.fillInStackTrace()); }}Copy the code

Among them, the failure to update the order record operations by itemKillSuccessMapper. ExpireOrder (info. GetCode ()); The corresponding dynamic Sql is written as follows:

<! Update order information --> <update id="expireOrder">
  UPDATE item_kill_success
  SET status = -1
  WHERE code = #{code} AND status = 0
</update>
Copy the code


(4) At this point, the RabbitMQ dead-letter queue message model code is done! Finally, I just need to call it at the place where “the message is sent to the dead-letter queue at the moment when the user successfully creates the order”, and the call code is as follows:

/** * The generic method - record the order generated after the user seckills successfully - and notify * @param of the asynchronous mail messagekill
 * @param userId
 * @throws Exception
 */
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{//TODO: Records the second kill order record generated after the successful purchase ItemKillSuccess Entity = New ItemKillSuccess(); String orderNo=String.valueOf(snowFlake.nextId()); //entity.setCode(RandomUtil.generateOrderCode()); // Traditional timestamp +N bits random number entity.setCode(orderNo); // Entity. SetItemId (kill.getitemId ()); entity.setKillId(kill.getId()); entity.setUserId(userId.toString()); entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue()); entity.setCreateTime(DateTime.now().toDate()); //TODO: double check lock writing modelled on singleton patternif (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
        int res=itemKillSuccessMapper.insertSelective(entity);

        if(res > 0) {/ / TODO: asynchronous message notification = the rabbitmq + mail rabbitSenderService. SendKillSuccessEmailMsg (orderNo); / / TODO: into the dead-letter queue for "failure" exceed the specified TTL rabbitSenderService. Time still did not pay order sendKillSuccessOrderExpireMsg (orderNo); }}}Copy the code


Finally, the test is: click on the “up” button, the user after the success of the kill, sends a message into the dead-letter queue (this can be seen in the RabbitMQ back-end console can) is a Ready good news, waiting for the 20 s, you can see the message transferred to real queue, and is the real consumer consumption monitoring, as shown below:

RabbitMQ dead letter queue this way can be very flexible to outstanding orders, and the whole process is “automatic”, without the need to manually click the button triggered! Of course, not everything is perfect, and the same is true of dead-letter queues. In an article we’ll cover the pitfalls of this approach and how to fix them!

Supplement:

1, at present, the overall construction of the second kill system and code combat has been completed, the complete source code database address can be downloaded here: gitee.com/steadyjack/… Remember Fork and Star!!

2. Finally, pay attention to Debug’s wechat official account: