RabbitMQ dead letter queue and message idempotent problem

Dead letter queue

1. Background of dead letter queue

RabbitMQ dead letter queue, commonly known as spare tire queue; After the message middleware rejects the message for some reason, it can be transferred to the dead letter queue for storage. The dead letter queue can also have switches, routing key s, etc.

2. Causes of dead letter queue

  • The message is delivered to MQ and stored. The message has expired. The consumer does not get our message in time. If the message is stored in MQ server and expired, it will be transferred to the spare wheel dead letter queue for storage.
  • The queue has reached its maximum length (the queue container is full)
  • If the consumer fails to consume multiple messages, they will be transferred and stored in the dead letter queue

3. Architecture principle of dead letter queue

Dead letter queues are not very different from ordinary queues. Ordinary and dead letter queues have their own independent switches and routing key s, queues and consumers.

The main differences are as follows

  • 1. The message delivered by the producer is delivered to our ordinary switch first. The ordinary switch caches the message in the ordinary queue. The ordinary queue corresponds to its own independent ordinary consumers.
  • 2. If the producer delivers a message to the ordinary queue and the ordinary queue finds that the message has not been consumed by the consumer, it will transfer the message to the dead letter (spare tire) switch. The dead letter (spare tire) switch has its own independent dead letter (spare tire) queue corresponding to the independent dead letter (spare tire) consumer.

4. Application scenario of dead letter queue

30 minute order timeout design

  1. Redis expired key:
  2. Implementation of dead letter delay queue: use dead letter queue to create a common queue without corresponding consumer consumption message. After 30 minutes, the message will be transferred to dead letter spare tire consumers for consumption. The spare wheel dead letter consumer will query whether it has been paid according to the order number. If it has not been paid, it will start rolling back the inventory operation
     /**
         * Declare dead letter switch
         *
         * @return DirectExchange
         */
        @Bean
        public DirectExchange dlxExchange() {
            return new DirectExchange(dlxExchange);
        }
        /**
         * Declare dead letter queue
         *
         * @return Queue
         */
        @Bean
        public Queue dlxQueue() {
            return new Queue(dlxQueue);
        }
        /**
         * Declaration order service switch
         *
         * @return DirectExchange
         */
        @Bean
        public DirectExchange orderExchange() {
            return new DirectExchange(orderExchange);
        }
        /**
         * Declare order queue
         *
         * @return Queue
         */
        @Bean
        public Queue orderQueue() {
            // The order queue is bound to our dead letter switch
            Map<String, Object> arguments = new HashMap<>(2);
            arguments.put("x-dead-letter-exchange", dlxExchange);
            arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
            return new Queue(orderQueue, true, false, false, arguments);
        }
        /**
         * Bind dead letter queue to dead letter switch
         *
         * @return Binding
         */
        @Bean
        public Binding binding() {
            return BindingBuilder.bind(dlxQueue())
                    .to(dlxExchange())
                    .with(dlxRoutingKey);
        }
        /**
         * Bind order queue to order switch
         *
         * @return Binding
         */
        @Bean
        public Binding orderBinding() {
            return BindingBuilder.bind(orderQueue())
                    .to(orderExchange())
                    .with(orderRoutingKey);
        }

Message idempotent problem

1. RabbitMQ message automatic retry mechanism

When our consumer processes and executes our business code, if an exception is thrown, mq will automatically trigger the retry mechanism. By default, rabbitmq is an unlimited number of retries. You need to specify the retry limit manually

2. Under what circumstances do consumers need to implement the retry strategy

A. after consumers get the message, they call the third party interface, but fail to call the third party interface. Do you want to try again?

In this case, the retry strategy needs to be implemented. The network delay is only temporarily unavailable, and the call may be called after multiple retries.

B. After the consumer gets the message, a data exception is thrown due to a code problem. Do you need to try again?

In this case, there is no need to implement the retry strategy. Even if you retry multiple times, you will eventually fail. The logs can be stored in the form of scheduled tasks or manual compensation in the later stage. If the message is retried multiple times or failed, the consumer version needs to be republished to realize consumption. You can use the dead letter queue

During the retry process of Mq, it may cause the problem of repeated consumption by consumers. Mq consumers need to solve the problem of idempotency to ensure the uniqueness of data

Solution

When delivering a message, the producer generates a globally unique id and puts it in our message. The consumer obtains the message and can query the database to repeat it according to the globally unique id. The global unique id is determined according to the business. For example, the order number is used as the global id, and then the data anti duplication is solved at the db level. The unique primary key constraint is used in the insert operation and the optimistic lock is used in the update operation

When an exception is thrown in the consumer's business logic code, it will automatically retry (the default is countless retries)

The number of RabbitMQ retries should be limited, for example, 5 retries at most, with an interval of 3s; In case of multiple retries or failures, it shall be stored in the dead letter queue or in the database table to record the later manual compensation

spring:
  rabbitmq:
    ####Connection address
    host: 127.0.0.1
    ####Port number
    port: 5672
    ####account number
    username: aaa
    ####password
    password: aaa
    ### address
    virtual-host: /zhangsan
    listener:
      simple:
        retry:
          ####Open the consumer (it will retry if the program is abnormal)
          enabled: true
          ####max retries 
          max-attempts: 5
          ####Retry interval: 3 seconds
          initial-interval: 3000
        acknowledge-mode: manual #Enable message sending confirmation
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")
public class FanoutOrderConsumer {

    @Autowired
    private OrderManager orderManager;
    @Autowired
    private OrderMapper orderMapper;

    @RabbitHandler
    public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
        try {
        log.info(">>orderEntity:{}<<", orderEntity.toString());
        String orderId = orderEntity.getOrderId();
        if (StringUtils.isEmpty(orderId)) {
            return;
        }
        OrderEntity dbOrderEntity = orderMapper.getOrder(orderId);
        if (dbOrderEntity != null) {
            log.info("In addition, the consumer has processed the business logic");
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            return;
        }
        int result = orderManager.addOrder(orderEntity);
        int i = 1 / 0;
        log.info(">>Insert data in database succeeded<<");
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // Record the message in the form of log and store it in the database db. In the middle and later stages, message compensation and manual compensation are realized through scheduled tasks

            //Store the message in the dead letter queue and write a dead letter consumer separately to realize consumption.
        }
    }
}

Look at this code. Before adding the database, first check the data in the database through the global id. if there is this data, directly confirm it with a message, and then set a unique constraint in the database to solve the idempotent problem caused by consumers

Keywords: Java RabbitMQ

Added by redesigner on Wed, 22 Dec 2021 16:36:28 +0200