Java Second Kill System Actual Series ~RabbitMQ Dead Letter Queue Processing Overtime Unpaid Orders

Summary:
This blog is the tenth in the "Java Second Kill System Practical Series Articles". In this blog, we will adopt RabbitMQ's death letter queue to deal with the situation that "user's death letter queue generates orders successfully but fails to pay late", so we can see the strength of RabbitMQ's death letter queue in the actual business environment.

Contents:
For the message middleware RabbitMQ, Debug has been briefly introduced in the previous chapters, and will not be repeated here! In this paper, we will use RabbitMQ's dead letter queue to achieve such business requirements: "After the user succeeds in killing seconds and successfully creates an order record, it should theoretically perform the operation to pay, but there is a situation that the user is reluctant to pay ~as for the reason, it is not known!"

For this scenario, you can experience in some mall platforms, that is, after choosing the goods, joining the shopping cart, Click to settle the account, then there will be a countdown, reminding you that you need to complete the payment within the specified time, otherwise the order will expire!

For this kind of business logic processing, the traditional way is to use the "timer" method, poll regularly to obtain orders that have exceeded the specified time, and then carry out a series of processing measures (such as striving to send short messages to users, reminding them how long the order will expire, etc.). In this second kill system, we will use RabbitMQ dead letter queue as a component to implement "invalidation" measures for this order!

"Dead letter queue", as we all know, is a special queue that can delay and delay a certain time to process messages. Compared with "ordinary queue", it can realize the function of "message entering the dead letter queue is not processed immediately, but can wait for a certain time to process". Ordinary queues do not work, that is, messages entering the queue are immediately monitored and consumed by the corresponding consumers. The basic message model of ordinary queues is shown in the following figure:

As for the "dead letter queue", its composition and use are relatively complex. Normally, the dead letter queue consists of three core components: dead letter switch + dead letter routing + TTL (message survival time ~unnecessary), and the dead letter queue can be "basic switch for producer + basic routing". So the producer first sends the message to the message model bound by "basic switch + basic routing". That is to say, it indirectly enters the dead letter queue. After TTL, the message will "hang up" and enter the next transit station, that is, "the dead letter switch of the consumer below + dead letter routing". ” The message model is bound. As shown in the following figure:

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

(1) First, we need to create the message model of the dead letter queue in the RabbitmqConfig configuration class, whose complete source code is as follows:

//Constructing a Dead Letter Queue Message Model with Order Timeout Unpaid after Successful Second Kill

@Bean
public Queue successKillDeadQueue(){
    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);
}

//Basic switch
@Bean
public TopicExchange successKillDeadProdExchange(){
    return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange"),true,false);
}
//Create Basic Switch + Basic Routing - > Dead Letter Queue Binding
@Bean
public Binding successKillDeadProdBinding(){
    return BindingBuilder.bind(successKillDeadQueue()).to(successKillDeadProdExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key"));
}
//Real queue
@Bean
public Queue successKillRealQueue(){
    return new Queue(env.getProperty("mq.kill.item.success.kill.dead.real.queue"),true);
}
//Dead Message Exchange
@Bean
public TopicExchange successKillDeadExchange(){
    return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.exchange"),true,false);
}
//Dead Message Switch + Dead Message Routing - > Binding of Real Queue
@Bean
public Binding successKillDeadBinding(){
    return BindingBuilder.bind(successKillRealQueue()).to(successKillDeadExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.routing.key"));
}

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

#Automated Failure-Dead Letter Queue Message Model with Order Timeout and Unpaid
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

#Unit is ms
mq.kill.item.success.kill.expire=20000

(2) After the message model has been successfully created, we need to develop the function of "sending messages into the dead letter queue" in the general Rabbit Sender Service class of AbbitMQ Sender Service. In this function method, we specify the message survival time TTL, and take the value as the configuration variable: mq. kill. item. success. The value of.Expire is 20s; the complete source code is as follows:

//After the second kill succeeds, a rush order is generated - sending information to a dead letter queue, waiting for an order that has expired for a certain time and has not been paid.
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 dynamically (for testing convenience, set 20s temporarily)
                        mp.setExpiration(env.getProperty("mq.kill.item.success.kill.expire"));
                        return message;
                    }
                });
            }
        }
    }catch (Exception e){
        log.error("Sequence killing success generates snap-up orders-Send messages to dead-letter queues, waiting for unpaid orders that expire for a certain period of time-An exception occurred with the message:{}",orderCode,e.fillInStackTrace());
    }
}

From the code of "sending messages to the dead-letter queue", we can see that messages first enter the message model of the dead-letter queue bound by "basic switch + basic routing"! When the message arrives at TTL, it will naturally emerge from the dead-letter queue (that is, "liberated") and then enter the next transit station, that is, "dead-letter switch + dead-letter routing" bound to the real queue of the message model, and ultimately the real consumer monitoring consumption!

At this point, you can run the whole project and system in the external tomcat server, then open the RabbitMQ back-end console application, find the dead letter queue, you can see the details of the dead letter queue, as shown in the following figure:

(3) Finally, we need to listen to and process the messages in the "real queue" in RabbitMQ's general message listening service class RabbitReceiver Service: Here we invalidate the order (provided that payment has not yet been made!). The complete source code is as follows:

//Unpaid timeouts after successful second killing - listener
@RabbitListener(queues = {"${mq.kill.item.success.kill.dead.real.queue}"},containerFactory = "singleListenerContainer")
public void consumeExpireOrder(KillSuccessUserInfo info){
    try {
        log.info("Failure to pay for overtime after successful second killing-monitor-receive messages:{}",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("Failure to pay for overtime after successful second killing-monitor-Abnormal:",e.fillInStackTrace());
    }
}

Among them, the operation of failure update order record is implemented by itemKillSuccessMapper.expireOrder(info.getCode()), and its corresponding dynamic Sql is written as follows:

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

(4) So far, the code battle for the RabbitMQ Dead Letter Queue Message Model is over! Finally, I only need to call it in the place of "the moment the user succeeds in creating the order, send the message to the dead letter queue". The calling code is as follows:

/**
 * A generic approach - recording orders generated after a user's second killing success - and notifying asynchronous mail messages
 * @param kill
 * @param userId
 * @throws Exception
 */
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{
    //TODO: Record the secondkill order records generated after a successful snap-up

    ItemKillSuccess entity=new ItemKillSuccess();
    String orderNo=String.valueOf(snowFlake.nextId());

    //entity.setCode(RandomUtil.generateOrderCode()); //Traditional timestamp + N-bit random number
    entity.setCode(orderNo); //Snowflake algorithm
    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: Learn to Use, cite one example and the other three - > Double Check Lock Writing by Simulating the Single Case Model
    if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
        int res=itemKillSuccessMapper.insertSelective(entity);

        if (res>0){
            //TODO: Notification for asynchronous mail messages = rabbitmq+mail
            rabbitSenderService.sendKillSuccessEmailMsg(orderNo);

            //TODO: Dead Letter Queue for orders that are still unpaid when "invalidation" exceeds the specified TTL time
            rabbitSenderService.sendKillSuccessOrderExpireMsg(orderNo);
        }
    }
}

Finally, self-test: click the "snap-up" button, the user will send a message to the dead-letter queue after the second kill is successful (this can be seen in the RabbitMQ back-end console is a positive Ready good news), wait 20 seconds, you can see the message transferred to the real queue, and be monitored by the real consumer consumption. As follows:

Okay, the introduction of "RabbitMQ Dead Letter Queue" and the application of actual combat are introduced here for the time being. This way can flexibly handle "overtime unpaid orders" and the whole process is "automatic and natural" without manual clicking on the button to trigger! Of course, everything is not perfect, so is the dead letter queue. In an article, we will introduce the flaws of this way and adopt corresponding solutions to deal with them.

Supplement:

1. Since the updates of the corresponding blogs may not be very fast, if you want to get a quick start and a real-world system, you can refer to read: Design and Practical Video Tutorial of Java Mall Secondary Kill System (SpringBook Edition)

2. At present, the whole construction and code battle of this second kill system have been completed. The complete source code database address can be downloaded here: https://gitee.com/steadyjack/SpringBoot-SecondKill

Keywords: Java RabbitMQ Tomcat SQL

Added by llimllib on Tue, 27 Aug 2019 07:33:20 +0300