SpringCloudAlibaba微服务消息调用:跨服务业务异步解耦
作者:黒漂技术佬 适用读者:了解SpringCloud基础,想在微服务架构中用好MQ的同学
一、微服务架构中MQ到底解决什么问题?
先说个真实的痛点。
你有一套无人售货柜微服务系统,拆成5个服务:订单服务、库存服务、支付服务、出货服务、设备监控服务。如果服务之间全用HTTP/Feign直接调用,链路大概长这样:
markdown
用户扫码开门 → 订单服务创建订单 → HTTP调库存服务扣库存 → HTTP调出货服务下发指令
→ HTTP调支付服务发起支付 → 支付成功 → HTTP回调订单服务
→ HTTP调出货服务执行出货 → HTTP调监控服务记录日志
问题来了:
- 同步阻塞:扣库存慢了,整条链路都卡住,用户站在柜子前干等
- 强耦合:库存服务挂了,订单服务也跟着挂
- 雪崩风险:一个服务超时,调用方不断重试,线程池打满,整个系统连锁倒塌
- 扩展难:加一个营销服务,得改一堆已有服务的代码加调用逻辑
MQ登场:服务之间不直接调,而是通过消息异步通信。上游服务发消息就完事,下游服务自己消费,彼此不知道对方存在。
markdown
订单服务 →(创建订单消息)→ RocketMQ → 库存服务消费
→ 营销服务消费(新增的,老服务不用改)
这就是异步解耦,MQ在微服务中最核心的价值。
二、SpringCloudAlibaba + RocketMQ整合
SpringCloudAlibaba技术栈里,注册中心用Nacos,消息中间件用RocketMQ,两者配合天衣无缝。
2.1 整体架构
markdown
┌──────────┐
│ Nacos │ 服务注册发现 + 配置中心
└────┬─────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────┴───┐ ┌─────┴────┐ ┌──────┴───┐
│订单服务 │ │ 库存服务 │ │ 支付服务 │
│Producer │ │ Consumer │ │ Producer │
└────┬───┘ └──────────┘ └────┬─────┘
│ │
│ ┌──────────────┐ │
└────→│ RocketMQ │←────────┘
│ NameServer │
│ + Broker │
└──────┬───────┘
│
┌──────────┼──────────┐
│ │ │
┌─────┴──┐ ┌────┴───┐ ┌───┴─────┐
│出货服务 │ │监控服务 │ │运营服务 │
│Consumer │ │Consumer│ │Consumer │
└────────┘ └────────┘ └─────────┘
2.2 核心依赖
每个微服务的 pom.xml 需要引入:
xml
<!-- SpringCloudAlibaba BOM -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2022.0.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- RocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
<!-- Sentinel 流控 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
2.3 核心配置
yaml
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: ${spring.application.name}-producer-group
小贴士:
spring.application.name作为producer group前缀,保证每个服务的group名不冲突。
三、无人售货柜微服务消息流转设计
这是本文的核心------把一个售货柜业务场景拆解清楚。
3.1 完整消息流转
scss
用户扫码开门
│
▼
┌──────────┐ (1)创建订单消息 ┌──────────┐
│ 订单服务 │ ──────────────────→ │ 库存服务 │ 扣减库存
│ │ └──────────┘
│ │ (2)开门指令消息 ┌──────────┐
│ │ ──────────────────→ │ 设备服务 │ 控制电磁锁开门
│ │ └──────────┘
└──────────┘
│
│ 用户关门
▼
┌──────────┐ (3)支付成功消息 ┌──────────┐
│ 支付服务 │ ──────────────────→ │ 订单服务 │ 更新订单状态
│ │ └──────────┘
│ │ (4)出货指令消息 ┌──────────┐
│ │ ──────────────────→ │ 出货服务 │ 控制电机出货
│ │ └──────────┘
│ │ (5)交易上报消息 ┌──────────┐
│ │ ──────────────────→ │ 运营服务 │ 统计分析
│ │ └──────────┘
│ │ (6)设备状态消息 ┌──────────┐
│ │ ──────────────────→ │ 监控服务 │ 监控告警
│ │ └──────────┘
└──────────┘
关键点:每个服务只负责发自己的消息 + 消费自己关心的消息。谁也不调谁的接口。
3.2 消息流转清单
| 步骤 | 生产者 | Topic | Tag | 消费者 |
|---|---|---|---|---|
| (1) | 订单服务 | order_topic | order_created | 库存服务 |
| (2) | 订单服务 | device_downward | open_door | 设备服务 |
| (3) | 支付服务 | payment_topic | payment_success | 订单服务 |
| (4) | 支付服务 | shipment_topic | ship_command | 出货服务 |
| (5) | 支付服务 | transaction_topic | transaction_report | 运营服务 |
| (6) | 设备服务 | device_upward | status | 监控服务 |
四、消息契约设计
微服务消息通信,最怕的是上下游对消息格式的理解不一致 。比如库存服务认为消息体里有 quantity 字段,但订单服务发出来的叫 qty------直接报错。
4.1 统一消息体格式
所有微服务共享一套Message DTO,建议抽成独立jar包(比如 vending-common-message),所有服务依赖它:
java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeviceMessage implements Serializable {
/** 消息唯一ID,用于幂等 */
private String messageId;
/** 消息来源服务名 */
private String sourceService;
/** 事件类型 */
private String eventType;
/** 业务数据载荷(JSON) */
private Map<String, Object> payload;
/** 时间戳 */
private Long timestamp;
/** 消息版本号 */
private String version;
}
4.2 Topic和Tag命名规范
命名规则:
Topic:{业务域}_topic 例如 order_topic, payment_topic
Tag: {具体事件} 例如 order_created, payment_success
好处:
- Topic粒度大,减少Topic数量(RocketMQ建议Topic不要太多)
- Tag做细粒度过滤,消费者可按Tag订阅
- 一看名字就知道什么业务、什么事件
生产者发送时带Tag的写法:
java
// topic:tag 格式
rocketMQTemplate.syncSend("order_topic:order_created", message);
消费者按Tag过滤:
java
@RocketMQMessageListener(
topic = "order_topic",
selectorExpression = "order_created || order_cancelled",
consumerGroup = "inventory-service-consumer"
)
4.3 消息版本管理
消息结构演进时,加字段可以兼容,删字段或改类型不能兼容。通过 version 字段让消费者可以判断如何处理:
java
DeviceMessage msg = DeviceMessage.builder()
.messageId(UUID.randomUUID().toString())
.sourceService("order-service")
.eventType("order_created")
.payload(payloadMap)
.timestamp(System.currentTimeMillis())
.version("1.0")
.build();
消费者如果收到不兼容的版本,记录日志并跳过,不要抛异常导致无限重试。
五、代码实战:订单→库存的跨服务消息
5.1 订单服务(生产者)
java
@Service
@Slf4j
public class OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderMapper orderMapper;
public void createOrder(OrderRequest request) {
// 1. 本地业务:创建订单
Order order = buildOrder(request);
orderMapper.insert(order);
// 2. 发送消息通知库存服务扣减库存
Map<String, Object> payload = new HashMap<>();
payload.put("orderId", order.getOrderId());
payload.put("productId", order.getProductId());
payload.put("quantity", order.getQuantity());
payload.put("deviceId", order.getDeviceId());
DeviceMessage message = DeviceMessage.builder()
.messageId(UUID.randomUUID().toString())
.sourceService("order-service")
.eventType("order_created")
.payload(payload)
.timestamp(System.currentTimeMillis())
.version("1.0")
.build();
rocketMQTemplate.syncSend("order_topic:order_created",
MessageBuilder.withPayload(message).build());
log.info("订单创建消息已发送: orderId={}", order.getOrderId());
}
}
5.2 库存服务(消费者)
java
@Slf4j
@Component
@RocketMQMessageListener(
topic = "order_topic",
selectorExpression = "order_created",
consumerGroup = "inventory-service-consumer-group",
messageModel = MessageModel.CLUSTERING
)
public class InventoryOrderConsumer implements RocketMQListener<DeviceMessage> {
@Autowired
private InventoryService inventoryService;
@Override
public void onMessage(DeviceMessage message) {
log.info("库存服务收到订单消息: messageId={}", message.getMessageId());
Map<String, Object> payload = message.getPayload();
String productId = (String) payload.get("productId");
Integer quantity = (Integer) payload.get("quantity");
// 幂等检查:用messageId防重复消费
if (inventoryService.isProcessed(message.getMessageId())) {
log.info("消息已处理过,跳过: messageId={}", message.getMessageId());
return;
}
// 业务:扣减库存
boolean success = inventoryService.deduct(productId, quantity);
if (!success) {
throw new RuntimeException("库存扣减失败,触发重试");
}
// 标记已处理
inventoryService.markProcessed(message.getMessageId());
}
}
幂等是微服务消息消费的必做题 。MQ的"至少一次"投递语义决定了消费者可能收到重复消息。用
messageId或业务唯一键做去重是最简单的方案。
六、跨服务消息的事务考虑
6.1 问题:本地事务+消息发送的原子性
看这段代码:
java
public void createOrder(OrderRequest request) {
orderMapper.insert(order); // ① 本地事务:写数据库
rocketMQTemplate.syncSend("order_topic", msg); // ② 发消息
}
如果第①步成功、第②步失败(网络抖动),订单创建了但库存不扣------数据不一致。如果第②步成功、第①步回滚了,库存扣了但订单没有------更严重。
6.2 最终一致性方案
| 方案 | 做法 | 优缺点 |
|---|---|---|
| 本地消息表 | 本地事务时同时写消息表,定时任务扫描发送MQ | 可靠但复杂,有延迟 |
| 事务消息 | RocketMQ原生事务消息,两阶段提交 | 原生支持,推荐(下篇详解) |
| Saga/TCC | 补偿型事务 | 重量级,适合复杂事务链路 |
本篇先给一个本地消息表的简单实现思路:
java
@Transactional
public void createOrder(OrderRequest request) {
// 同一个数据库事务里:写订单 + 写待发送消息
Order order = buildOrder(request);
orderMapper.insert(order);
LocalMessage localMessage = new LocalMessage();
localMessage.setMessageId(UUID.randomUUID().toString());
localMessage.setTopic("order_topic");
localMessage.setTag("order_created");
localMessage.setBody(JSON.toJSONString(buildPayload(order)));
localMessage.setStatus("PENDING");
localMessageMapper.insert(localMessage);
// 事务提交后,定时任务扫描 status=PENDING 的消息发送到MQ
}
// 定时任务,每5秒扫描一次
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<LocalMessage> messages = localMessageMapper.selectByStatus("PENDING");
for (LocalMessage msg : messages) {
try {
rocketMQTemplate.syncSend(
msg.getTopic() + ":" + msg.getTag(),
msg.getBody()
);
localMessageMapper.updateStatus(msg.getMessageId(), "SENT");
} catch (Exception e) {
log.warn("消息发送失败,下次重试: {}", msg.getMessageId());
}
}
}
这样保证了本地事务和消息发送的最终一致性。RocketMQ原生事务消息是更优雅的方案,下一篇专门讲。
七、消息流量控制:Sentinel + RocketMQ
当售货柜搞大促,订单消息突然涨了10倍,消费者扛不住怎么办?用Sentinel给消费者加限流。
7.1 消费者限流
RocketMQ消费者线程池默认20个线程,配合Sentinel可以在消费方法上做流控:
java
@Slf4j
@Component
@RocketMQMessageListener(
topic = "order_topic",
consumerGroup = "inventory-service-consumer-group"
)
public class InventoryOrderConsumer implements RocketMQListener<DeviceMessage> {
@Override
@SentinelResource(
value = "consumeOrderMessage",
blockHandler = "onBlock"
)
public void onMessage(DeviceMessage message) {
// 正常消费逻辑
doBusiness(message);
}
/**
* Sentinel限流后的兜底处理
* 注意:这里不能吞掉异常,要让消息重试
*/
public void onBlock(DeviceMessage message, BlockException ex) {
log.warn("消费被限流,消息将稍后重试: messageId={}",
message.getMessageId());
throw new RuntimeException("消费限流", ex);
}
}
Sentinel控制台可以配置规则,比如:consumeOrderMessage 资源的QPS阈值设为100,超过就限流。被限流的消息抛异常后触发RocketMQ重试机制,等流量高峰过去后自动恢复。
7.2 生产者限流
生产者发送端也可以加Sentinel保护,防止短时间内发送过多消息压垮Broker:
java
@SentinelResource(value = "sendOrderMessage", blockHandler = "onSendBlock")
public SendResult sendOrder(OrderDTO order) {
return rocketMQTemplate.syncSend("order_topic", order);
}
public SendResult onSendBlock(OrderDTO order, BlockException ex) {
log.warn("发送被限流,订单降级处理: orderId={}", order.getOrderId());
// 降级:写入本地消息表稍后补偿
localMessageMapper.insert(buildLocalMessage(order));
return null;
}
八、总结
微服务 + MQ 的核心套路:
markdown
1. 异步解耦:服务之间不直接调用,通过消息通信
2. 消息契约:统一DTO + Topic/Tag命名规范 + 版本管理
3. 幂等消费:用messageId或业务唯一键去重
4. 最终一致:本地消息表或事务消息保证数据一致
5. 流量控制:Sentinel保护生产者和消费者
记住这张消息流转图,设计微服务消息架构时心里就有框架了。售货柜场景里6条消息链路,每条都是"生产者发→消费者收→处理业务→可能再发下一条消息"的接力赛。把接力棒交接清楚(消息契约),比赛就不会出乱子。