A Guide to RabbitMQ Java Client
In this post, I will cover the common uses of the RabbitMQ Java client.
Dependencies
Before using the RabbitMQ Java client, you need to add the RabbitMQ Java client dependencies to your project:
// Gradle (Kotlin DSL) |
or
<!-- Maven --> |
Running RabbitMQ with Docker
docker run -d --name rabbitmq1 -p 5672:5672 -p 15672:15672 rabbitmq:4-management |
Establishing a connection to RabbitMQ
An Advanced Message Queuing Protocol (AMQP) connection is a link between the client and the broker that performs underlying networking tasks, including initial authentication, IP resolution, and networking.
Each AMQP connection maintains a set of underlying channels. A channel reuses a connection, forgoing the need to reauthorize and open a new TCP stream, making it more resource-efficient.
Unlike creating channels, creating connections is a costly operation, very much like it is with database connections. A single AMQP connection can be used by many threads through many multiplexed channels. The handshake process for an AMQP connection requires at least seven TCP packets, and even more when using TLS. Channels can be opened and closed more frequently if needed.
The following is an example of establishing a RabbitMQ connection using the Java client:
ConnectionFactory factory = new ConnectionFactory(); |
Publishing messages
RabbitMQ supports four main types of exchanges, each with different routing logic to determine how messages are delivered to queues. You send messages to an exchange, which uses the routing key and bindings to determine which queue(s) to deliver the message to.
The four different types of exchanges:
- Direct: Routes messages to queues with an exact match between the routing key and the queue’s binding key. Delivers messages to a single queue.
- Topic: Routes messages to queues based on pattern-matching between the routing key and binding key, using
*
and#
wildcards (*
: matches exactly one word,#
: matches zero or more words). Delivers messages to multiple queues based on pattern-matching routing keys. - Fanout: Messages are routed to all queues bound to the fanout exchange. Ignores routing keys.
- Headers: Use the message header attributes for routing. Ignores routing keys.
Using a direct exchange to route messages to a single queue
// Create a connection and a channel |
Using the RabbitMQ default exchange (a direct exchange)
// Create a connection and a channel |
Using a topic exchange to route messages to multiple queues
// Create a connection and a channel |
Using a fanout exchange to route messages to all queues bound to the fanout exchange
// Create a connection and a channel |
Consuming messages
Connection connection = MyHelper.getConnection(); |
Advanced features
Handling dead letters
Not all messages are consumed every day, which leads to messages piling up in queues. Though the amount of data is not detrimental to the system, having messages lying around in queues, potentially forever, is not a good idea. Imagine a user logging in after a couple of weeks of vacation and being flooded with obsolete messages—this is the negative type of user experience that should be avoided.
A Dead Letter refers to a message that cannot be delivered, processed, or acknowledged, and is therefore redirected to a Dead Letter Exchange (DLX) for special handling.
There are two common ways to handle dead letters:
- It will be emailed to the user if it’s an important information message.
- It will be discarded if it’s an information message for the current situation or non-important information.
The following is an example of moving expired messages to a Dead Letter Exchange:
// Get a connection and a channel |
Delayed messages
Sometimes you want to delay sending messages. For example, a taxi user survey should be sent out to customers 5 minutes after a finished ride.
The AMQP protocol doesn’t have a native delayed queue feature, but one can easily be emulated by combining the message TTL function and the dead-lettering function.
The following is an example of sending delayed messages:
// Get a connection and a channel |
Mandatory
If no queue is bound to the exchange with a matching routing key, the message is silently dropped.
The mandatory
flag is an option you can set when publishing a message, and it controls what happens if the message cannot be routed to any queue. The mandatory flag tells RabbitMQ: If you can’t route this message to at least one queue, return it to the sender instead of silently dropping it.
// Get a connection and a channel |
MyReturnListener
|
Settings
Specifying a consumer prefetch count
The number of messages sent to the consumer at the same time can be specified through the prefetch count value. The prefetch count value is used to get as much out of the consumers as possible. The default value of prefetch count in RabbitMQ is unlimited. By default, RabbitMQ will send as many messages as the consumer can buffer, without waiting for acknowledgments.
If the prefetch count is too small, it could negatively affect the performance of RabbitMQ, since the platform is usually waiting for permission to send more messages.
If the prefetch count is large, it makes RabbitMQ send many messages from one queue to one consumer. If all messages are sent to a single consumer, it may be overwhelmed and leave the other consumers idle.
Setting the correct prefetch value:
- Setting a high prefetch value: In a scenario of one or a few consumers who are quickly processing messages, it is recommended to prefetch many messages at once to keep the client as busy as possible.
- Setting a low prefetch value: A low prefetch value is recommended in situations where there are many consumers and a short processing time.
- Setting prefetch value to one: In scenarios where there are many consumers and/or a longer time to process messages, it is recommended that the prefetch value is set to one (1) to evenly distribute messages among all consumers.
Set the prefetch value for a message consumer channel:
channel.basicQos(prefetchCount); |
References
[1] RabbitMQ Essentials (2nd) by Lovisa Johansson and David Dossot