一、Bug 场景
在一个金融交易系统中,使用 RabbitMQ 作为消息中间件来处理交易相关的消息。例如,用户发起一笔股票交易,系统会依次发送 “订单创建”、“资金冻结”、“股票交割” 等消息。这些消息需要按照顺序被消费和处理,以确保交易流程的正确性。
二、代码示例
生产者代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class TradingProducer {
private static final String QUEUE_NAME = "trading_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String[] messages = {"订单创建", "资金冻结", "股票交割"};
for (String message : messages) {
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF - 8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
消费者代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;
public class TradingConsumer {
private static final String QUEUE_NAME = "trading_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.basicConsume(QUEUE_NAME, true,
"tradingConsumerTag",
(consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF - 8");
System.out.println(" [x] Received '" + message + "'");
processMessage(message);
},
consumerTag -> {
System.out.println("Consumer cancelled: " + consumerTag);
});
}
private static void processMessage(String message) {
System.out.println("Processing message: " + message);
}
}
三、问题描述
- 预期行为:消费者按照生产者发送消息的顺序依次接收和处理 “订单创建”、“资金冻结”、“股票交割” 消息,以保证交易流程的正确执行。
- 实际行为:在实际运行中,尤其是在高并发或者网络不稳定的情况下,消费者可能会乱序接收消息。例如,先接收到 “资金冻结”,然后是 “订单创建”,最后是 “股票交割”。这是因为 RabbitMQ 默认情况下不保证消息的顺序性。在消息的生产、传输和消费过程中,可能会由于网络波动、多个消费者竞争、消息在队列中的存储和转发机制等因素,导致消息的顺序发生变化。
四、解决方案
- 使用单个消费者:确保只有一个消费者从队列中接收消息,这样可以保证消息的顺序性。但这种方式无法充分利用多消费者的并行处理能力,可能会影响系统的整体性能。
- 分区队列:根据某个业务标识(如交易 ID)对消息进行分区,相同业务标识的消息发送到同一个队列,每个队列由一个消费者处理。这样可以在保证同一业务流程消息顺序的同时,利用多个队列和消费者实现并行处理。
修改后的生产者代码(使用分区队列)
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class TradingProducer {
private static final String EXCHANGE_NAME = "trading_exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String[] messages = {"订单创建", "资金冻结", "股票交割"};
String routingKey = "12345";// 假设交易 ID
for (String message : messages) {
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF - 8"));
System.out.println(" [x] Sent '" + message + "' with routing key '" + routingKey + "'");
}
}
}
}
修改后的消费者代码(使用分区队列)
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;
public class TradingConsumer {
private static final String EXCHANGE_NAME = "trading_exchange";
private static final String QUEUE_NAME = "trading_queue_12345";// 根据交易 ID 命名队列
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "12345");// 根据交易 ID 绑定队列
channel.basicConsume(QUEUE_NAME, true,
"tradingConsumerTag",
(consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF - 8");
System.out.println(" [x] Received '" + message + "'");
processMessage(message);
},
consumerTag -> {
System.out.println("Consumer cancelled: " + consumerTag);
});
}
private static void processMessage(String message) {
System.out.println("Processing message: " + message);
}
}
- 消息携带顺序标识:生产者在消息中添加顺序标识(如序列号),消费者在接收到消息后,根据标识对消息进行排序后再处理。这种方式在实现上相对复杂,需要额外的逻辑来管理和处理消息顺序。