How to operate in a distributed scenario to ensure that the message queue achieves ultimate consistency

Consider a common scenario in a distributed scenario: Service A sends a message to the message queue after successfully executing a certain database operation. Now I hope that this message will be sent only when the database operation is successfully executed. Here are some common practices:

1. Perform database operations before sending messages

 

public void purchaseOrder() {
    orderDao.save(order);
    messageQueue.send(message);
}

It is possible that the order is added successfully but the message sent fails. Eventually an inconsistent state is formed.

2. Send the message first, and then perform the database operation

 

public void purchaseOrder() {
    messageQueue.send(message);
    orderDao.save(order);
}

It is possible that the message is sent successfully but the order creation fails, resulting in an inconsistent state.

3. In the database transaction, send the message first, and then perform the database operation

 

@Transactional public void purchaseOrder() {
    messageQueue.send(message);
    orderDao.save(order);
}

There is also no guarantee of consistency here. If the database operation is successful, but the message has been sent, it cannot be rolled back.

4. In the database transaction, perform the database operation first, and then send the message

 

@Transactional public void purchaseOrder() {
    orderDao.save(order);
    messageQueue.send(message);
}

The success of this scheme depends on whether the message queue has a response mechanism and a transaction mechanism.

The response mechanism means that after the producer sends a message, the message queue can return a response to prove whether the message is inserted successfully.

If the message queue has a response mechanism, rewrite the above code as:

 

@Transactional public void purchaseOrder() {
    orderDao.save(order); try{
        kafkaProducer.send(message).get();
    } catch(Exception e) throw new RuntimeException("Fail to send message");
    }

This piece of code means that if you send and receive a message queue error response, a RuntimeException will be thrown. Then the message sending failure can cause the rollback of the database operation. This solution seems feasible, but there is such a situation, if the message is sent successfully, and the message queue does not immediately return a response due to network reasons, at this time the message sender does not receive the response in time and thinks that the message has failed, so the message is sent The database transaction of the party was rolled back, but the message was indeed inserted successfully, which caused the final inconsistency.

The above inconsistencies can be resolved through the message transaction mechanism.

The transaction mechanism indicates whether the message in the message queue has a state, thereby determining whether the consumer consumes the message.

Alibaba's open source message queue RocketMQ is known for its high availability. It is the first message queue to support transactional messages. Kafka also supports the transaction mechanism since version 0.11.

The transaction mechanism of RoketMQ is to mark the message as Prepared or Confirmed. Messages in the Prepared state are not visible to the consumer.

Kafka uses the Transaction Marker to mark the message as Uncommited or Commited. Consumers decide which types of messages are visible by configuring isolation-level to read_committed or read_uncommitted.

5. The message queue does not support transaction messages

If the message queue does not support transaction messages, then our solution is to add a message table, and start a scheduled task to scan this message table, and send all messages in the prepared state to the message queue. After the message is sent successfully, The message status is set to confirmed.

code show as below:

 

@Transactional public void purchaseOrder() {
    orderDao.save(order);
    messageService.save(message);
}

At this time, the logic of inserting the order and inserting the message is in the same database transaction, and the message table is continuously scanned through the background timing program, so it must be guaranteed that the message is successfully delivered to the message consumer.

One problem with this solution is that it is possible that the background task will go down after sending the message successfully, so there is no time to set the status of the sent message to confirmed. Therefore, the next time the message table is scanned, the message will be sent repeatedly. This is at least once delivery.

Due to the characteristics of at least once delivery, consumers may receive duplicate data. At this point, a message_consume table can be established on the consumer side to determine whether the message has been consumed. If it has been consumed, the message is directly discarded.

 

Guess you like

Origin blog.csdn.net/yunduo1/article/details/108623239