7 Common Message‑Queue Scenarios and How to Choose the Right Type
This article examines seven typical messaging scenarios—ordinary, ordered, delayed, transactional, trace, dead‑letter, and priority messages—explaining the problem each solves, the trade‑offs, and concrete implementations in RocketMQ, Pulsar, RabbitMQ and Kafka with code samples.
1. Ordinary Messages
Ordinary messages are the most basic use case: a producer sends a message, the broker stores it, and a consumer retrieves it, achieving system decoupling and peak‑shaving.
2. Ordered Messages
Ordered messages require the production and consumption order to be identical. In an e‑commerce flow—order creation, payment, and shipment—the consumer must process these events sequentially.
Ensuring order is difficult because:
Multiple producers experience different network latencies, so the broker cannot guarantee the write order.
Multiple partitions or queues break the global order.
Concurrent consumers on the same partition may process out of order.
Two conditions must be satisfied:
The same producer must synchronously send to the same partition.
Each partition must be consumed by only one consumer.
Kafka and Pulsar achieve this by hashing a key to a partition. RocketMQ provides a MessageQueueSelector to explicitly choose a queue.
Example with RocketMQ:
public static void main(String[] args) throws UnsupportedEncodingException {
try {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
String[] tags = new String[]{"TagA","TagB","TagC","TagD","TagE"};
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
e.printStackTrace();
}
}RabbitMQ achieves ordering through the exchange‑to‑queue routing based on a consistent routing key.
@Resource
private AmqpTemplate rabbitTemplate;
public void send1(String message) {
rabbitTemplate.convertAndSend("testExchange", "testRoutingKey", message);
}3. Delayed (Scheduled) Messages
Delayed messages are stored but not delivered until a specified time, e.g., canceling an unpaid order after 30 minutes.
3.1 RocketMQ Implementation
RocketMQ defines 18 delay levels; level 3 corresponds to a 10‑second delay.
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";When a producer sends a delayed message, the broker first stores it in a special topic SCHEDULE_TOPIC_XXXX. A scheduler checks the timestamp; once expired, the message is moved to the original queue for consumption.
RocketMQ 5.0 adds true timed messages via a time‑wheel algorithm, extending the maximum delay beyond two hours.
3.2 Pulsar Implementation
Pulsar writes delayed messages into a Delayed Message Tracker that builds a priority queue based on the delay time. Consumers first consult this tracker; if a message has expired, it is delivered immediately.
3.3 RabbitMQ Implementation
RabbitMQ offers two approaches:
Publish to a normal queue with a TTL; when the TTL expires, the message is dead‑lettered to a separate queue for consumption.
Store the message in a local Mnesia database and use a timer to forward it to the broker after expiration.
3.4 Kafka Implementation
Kafka lacks a native delay queue. Common work‑arounds include a producer interceptor that postpones send time or a dedicated delayed topic that mimics RocketMQ’s schedule‑topic pattern.
4. Transactional Messages
Transactional messages guarantee atomicity between message production and a related operation.
RabbitMQ and Kafka support producer‑side transactions only: either all messages in a batch are committed or none are.
RabbitMQ example using a channel transaction:
ConnectionFactory factory = new ConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// start transaction
channel.txSelect();
channel.basicPublish("directTransactionExchange", "transactionRoutingKey", null, message.getBytes("utf-8"));
// commit or rollback
channel.txCommit();Kafka can assign the same transaction ID to multiple producers, allowing atomic writes across several topics and partitions.
Pulsar defines a transaction as an atomic unit that spans consumption, processing, and production, thus covering the entire message flow.
RocketMQ implements transactions with a “half‑message” protocol: the producer sends a provisional message, executes a local transaction, and then commits or rolls back the half‑message based on the local outcome.
RocketMQ guarantees atomicity only between the producer’s send and its local transaction; consumer‑side atomicity is not provided.
5. Trace (Audit) Messages
Trace messages record the lifecycle of a message to aid debugging when loss occurs. They consume storage and query resources, so they should be used judiciously.
Record key lifecycle checkpoints.
Avoid degrading normal message throughput.
Do not overload broker storage.
Design query dimensions and performance expectations.
RabbitMQ provides a trace switch that forwards trace data to the amq.rabbitmq.trace exchange, but developers must implement the consumer side.
RocketMQ includes trace capabilities in producer, broker, and consumer components, but they are disabled by default and must be manually enabled.
6. Dead‑Letter Queues (DLQ)
DLQs handle abnormal situations such as message expiration, rejection, or queue overflow.
RocketMQ creates a consumer‑side DLQ: after 16 failed retries, the message is moved to the DLQ.
RabbitMQ supports both producer‑ and broker‑side DLQs. A message becomes dead‑lettered when:
The producer rejects it with requeue=false.
The broker’s TTL expires.
The queue reaches its maximum length.
Dead‑lettered messages are routed to a dead‑letter exchange.
7. Priority Messages
Some business scenarios require preferential processing, e.g., premium‑card customers in banking.
RabbitMQ supports priority queues via the x-max-priority argument:
ConnectionFactory factory = new ConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
Map<String, Object> args = new HashMap<String, Object>();
// set max priority to 5
args.put("x-max-priority", 5);
channel.queueDeclare("my-priority-queue", true, false, false, args);Conclusion
When selecting a message‑queue solution, consider the business scenario (ordering, delay, transaction, tracing, etc.) alongside operational factors such as deployment complexity, community activity, and learning curve. The analysis above provides a practical reference for matching requirements to the capabilities of RocketMQ, Pulsar, RabbitMQ, and Kafka.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
