Service Communication
Li Wei
Title: Service Communication
Overview
Once microservices are split, they inevitably need to call each other. Currently our inter‑service calls all use OpenFeign. In this style, the caller sends a request and waits for the provider to finish processing and return a result before continuing. In other words, the caller is blocked during the call, which we refer to as synchronous calling or synchronous communication. In many scenarios, however, we may want to use asynchronous communication.
Interpretation:
- Synchronous communication: A service initiates a call and waits for it to complete and return a result, blocking the current thread.
- Asynchronous communication: The caller sends a request and does not wait for the result immediately, but continues with other work.
If our business requires an immediate response from the provider, we should choose synchronous communication (synchronous call). If we aim for higher efficiency and do not need an immediate response, we should choose asynchronous communication (asynchronous call).
For asynchronous calls, the mechanism is based on message notification and generally involves three roles:
- Message sender: The entity that posts the message – the original caller.
- Message broker: Manages, stores temporarily, and forwards messages; you can think of it as a WeChat server.
- Message receiver: The entity that receives and processes the message – the original service provider.
In an asynchronous call, the sender no longer calls the receiver’s business interface directly. Instead, it posts a message to the broker. The receiver then subscribes to messages from the broker as needed. Whenever the sender posts a message, the receiver can fetch and process it.
Thus, the sender and receiver are completely decoupled.
Pros and cons of asynchronous calls:
- Advantages
- Lower coupling
- Better performance
- Stronger business scalability
- Fault isolation, preventing cascading failures
- Disadvantages
- Cannot obtain the call result immediately; timeliness is poorer
- Fully dependent on the broker’s reliability, security, and performance
- Architecture is more complex; maintenance and debugging are harder
Technology selection
Comparison of several common MQs:
| RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
|---|---|---|---|---|
| Company/Community | Rabbit | Apache | Alibaba | Apache |
| Development language | Erlang | Java | Java | Scala & Java |
| Protocol support | AMQP, XMPP, SMTP, STOMP | OpenWire, STOMP, REST, XMPP, AMQP | Custom protocol | Custom protocol |
| Availability | Medium | High | High | High |
| Single‑node throughput | Medium | Poor | High | Very high |
| Message latency | Microseconds | Milliseconds | Milliseconds | < millisecond |
| Message reliability | High | Medium | High | Medium |
When to choose
- Prioritize availability: Kafka, RocketMQ, RabbitMQ
- Prioritize reliability: RabbitMQ, RocketMQ
- Prioritize throughput: RocketMQ, Kafka
- Prioritize low latency: RabbitMQ, Kafka
RabbitMQ
(Download and open with XMind)
Basic Introduction
RabbitMQ is an open‑source message‑oriented middleware developed in Erlang.
Official website:
Roles in RabbitMQ:
publisher: Producerconsumer: Consumerexchange: Exchange, responsible for routing messagesqueue: Queue, stores messagesvirtualHost: Virtual host, isolates exchanges, queues, and messages of different tenants
Basic architecture:
Installation & Configuration
Example using a Docker container:
- Pull the image
- Run the container
15672: Port of RabbitMQ’s management console5672: Message publishing endpoint
After installation, visit http://主机地址:15672 to see the management console.
Note: Sending/receiving messages via the console is not demonstrated here; see the Black Horse (Heima) documentation for details.
Getting Started Demo
Publisher implementation – idea:
- Establish a connection
- Create a channel
- Declare a queue
- Publish a message
- Close the channel and connection
Code implementation:
Consumer implementation – idea:
- Establish a connection
- Create a channel
- Declare a queue
- Subscribe to messages
Code implementation:
Spring AMQP
Basic introduction
In the demo you can see that using a message queue for send/receive results in a lot of boilerplate code—much like the early days of JDBC. Fortunately, JDBC later got a well‑designed API that made it simple, and RabbitMQ enjoys the same benefit because it follows the AMQP protocol, which is language‑agnostic. Any language that implements AMQP can interact with RabbitMQ.
AMQP (Advanced Message Queuing Protocol) is an open standard for asynchronous message transfer. It defines how producers, consumers, and brokers communicate and the format of messages. AMQP provides reliable delivery, routing, queues, exchanges, and other features that enable flexible, dependable communication between disparate systems.
Key AMQP concepts:
- Producer: Sends messages to a queue or exchange.
- Consumer: Receives messages from a queue or exchange.
- Message broker: Middleware that receives, stores, routes, and forwards messages; typically includes queues and exchanges.
- Queue: Buffer that stores messages from producers and delivers them to consumers according to defined rules.
- Exchange: Receives messages from producers and routes them to one or more queues based on routing logic.
- Routing key: Keyword supplied by the producer that the exchange uses to decide where to route the message.
- Binding: Association between an exchange and a queue that determines how messages are routed.
- Virtual host: Logical container on an AMQP server that isolates different applications or users.
Spring AMQP wraps the AMQP protocol for Spring applications, providing a convenient template built on top of RabbitMQ and auto‑configuration via Spring Boot.
Spring AMQP official site: https://spring.io/projects/spring-amqp
Documentation: https://docs.spring.io/spring-amqp/docs/2.4.14/reference/html
Spring AMQP offers three main features:
- Automatic declaration of queues, exchanges, and bindings
- Annotation‑based listener model
@RabbitListenerfor asynchronous message reception (requires@Componenton the class) - A
RabbitTemplateutility for sending messages
Simple Queue Model – BasicQueues
In this model, messages are sent directly to a queue, bypassing any exchange (see diagram). The publisher posts directly to the queue, and the consumer listens and processes messages from that queue. This pattern is mainly for testing and rarely used in production.
Message sending
- Add the Maven dependency to the publisher service
- Add configuration in
application.ymlof the publisher - Write a test class
SpringAMQPTestand useRabbitTemplateto send messages
Message receiving
- Add the Maven dependency to the consumer service
- Add configuration in
application.ymlof the consumer - Create a
SpringRabbitListenerclass in the consumer’slistenerpackage to receive messages (code omitted) - Start the consumer service, then run the publisher test to send MQ messages; the console will show both send and receive logs
Work Queues Model – “the capable do more”
Work queues (also called task queues) let multiple consumers bind to a single queue and share the load.
When processing is time‑consuming, the production rate can far exceed the consumption rate, causing a backlog. Using the work‑queue model, multiple consumers process messages concurrently, dramatically increasing throughput.
Using the work model
- Multiple consumers bind to one queue; each message is processed by only one consumer
- Set
**prefetch**to control how many messages each consumer pre‑fetches
Message sending
Add a test method in the SpringAmqpTest class of the publisher service.
Message receiving
To simulate multiple consumers on the same queue, add two new methods to SpringRabbitListener in the consumer service. Both consumers are configured with Thead.sleep to simulate processing time:
- Consumer 1 sleeps 20 ms → ~50 messages/sec
- Consumer 2 sleeps 200 ms → ~5 messages/sec
Thus, messages are distributed evenly regardless of each consumer’s capacity, leading to one consumer being idle while the other is overloaded, and overall processing time exceeds one second.
“The capable do more”
Spring allows a simple configuration to address this. Add the following to the consumer’s application.yml:
(configuration omitted)
After restarting, the faster consumer processes more messages, while the slower one handles only six. Total execution time drops to around one second, demonstrating efficient utilization of each consumer’s capability and preventing backlog.
Publish/Subscribe Model – Basic Introduction
The previous two examples did not involve an exchange; producers sent directly to a queue. Introducing an exchange changes the flow dramatically (see diagram).
In the publish/subscribe model, an exchange appears, and the process is slightly different:
- Publisher: Sends messages to an exchange instead of directly to a queue.
- Exchange: Receives messages from the publisher and decides what to do—deliver to a specific queue, all queues, or discard—based on its type.
- Queue: Still stores and buffers messages, but must be bound to an exchange.
- Consumer: Subscribes to a queue as before.
Important: An exchange only forwards messages; it does not store them. If no queue is bound to the exchange, or no queue matches the routing rules, the message is lost!
Exchange types (four kinds):
- Fanout: Broadcasts the message to all queues bound to the exchange. This is the type we first used in the console.
- Direct: Routes based on
RoutingKey(路由key)to queues that have subscribed. - Topic: Like Direct, but the routing key can contain wildcards.
- Headers: Matches on message headers; used less frequently.
Fanout Exchange
“Fanout” literally means “spread out”; in MQ terminology it’s essentially a broadcast.
In broadcast mode, the message flow is:
(diagram omitted)
Characteristics
- Can have multiple queues
- Each queue must be bound to the exchange
- Producers send only to the exchange
- The exchange forwards the message to all bound queues
- All consumers of those queues receive the message
Direct Exchange
In Fanout mode, every subscribed queue receives each message. Sometimes we need different messages to go to different queues; that’s where a Direct exchange is used.
Characteristics
- Binding a queue to an exchange requires specifying a routing key
RoutingKey - When publishing, the producer must also specify a routing key
RoutingKey - The exchange forwards the message only to queues whose binding key
RoutingKeyexactly matches the message’s routing keyRoutingKey
Topic Exchange
Compared with Direct, a Topic exchange also routes based on the routing key, but it allows wildcards in the binding key.
A binding key typically consists of one or more words separated by . (e.g., item.insert).
Wildcard rules:
#: matches one or more words*: matches exactly one word
Examples:
item.#matchesitem.spu.insertoritem.spuitem.*matches onlyitem.spu
(diagram omitted)
Assume a producer sends messages with four possible routing keys:
china.news– Chinese newschina.weather– Chinese weatherjapan.news– Japanese newsjapan.weather– Japanese weather
Explanation:
topic.queue1binds tochina.#; any routing key starting withchina.matches, includingchina.newsandchina.weather.topic.queue2binds to#.news; any routing key ending with.newsmatches, includingchina.newsandjapan.news.
Declaring Queues and Exchanges
When you declare queues and exchanges in Java code, the framework checks at startup and creates them automatically if they do not exist.
Basic API
Spring AMQP provides a Queue class for creating queues:
(code omitted)
Spring AMQP also offers a Exchange interface representing all exchange types:
(code omitted)
You can create queues and exchanges yourself, but Spring AMQP supplies ExchangeBuilder to simplify the process:
(code omitted)
When binding a queue to an exchange, use BindingBuilder to create a Binding object:
(code omitted)
@Bean // fanout example
Create a class in the consumer to declare the queue and exchange:
(code omitted)
Direct example
Because Direct mode may require binding multiple KEY, it can become verbose—each key needs its own binding:
(code omitted)
@RabbitListener declaration – using @Bean to declare queues and exchanges can be cumbersome; Spring also supports annotation‑based declarations.
Direct example (annotation)
(code omitted)
Topic example
(code omitted)
Easy piece!
Message Converter
Spring’s sending code receives the message body as an Object:
(code omitted)
During transmission, Spring serializes the object to a byte stream for the MQ and deserializes it back to a Java object on receipt. By default, Spring uses JDK serialization.
It is well‑known that JDK serialization has several drawbacks:
- Performance – relatively slow, especially for complex objects or large payloads, because it traverses the entire object graph and creates many intermediate objects.
- Platform dependence – serialized data can differ across Java versions or operating systems, causing compatibility issues.
- Version compatibility – changes to class structure (adding/removing fields) may break deserialization or lead to data loss.
- Security – vulnerable to remote code execution (RCE) attacks via crafted serialized streams.
- Poor readability
Configuring a Message Converter
- Add the required dependencies to both
publisherandconsumerservices.- Note: If the project already includes
spring-boot-starter-web, you do not need to add Jackson again.
- Note: If the project already includes
- Configure the converter by adding a
Beanbean in the startup classes ofpublisherandconsumer.- Adding a
messageIdin the converter helps with future idempotency checks.
- Adding a
Message Reliability
Overview
Every step from producer to consumer can cause message loss:
- Loss when sending:
- Producer fails to connect to MQ (producer retry mechanism)
- Producer sends a message but the exchange is not found (producer confirm mechanism)
(content truncated)
Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.