RocketMQ 默认情况下不保证全局消息顺序,但在特定场景下可以实现局部有序。本文将深入探讨 RocketMQ 的顺序消息机制,并提供生产环境的实战方案。
消息顺序性的本质
在分布式系统中,消息顺序性分为两个层面:
1. 全局有序
所有消息严格按照发送顺序被消费,整个 Topic 只有一个队列,性能极低。
2. 局部有序
相同业务标识的消息保持顺序,不同业务标识的消息可以并行处理。

RocketMQ 顺序消息的实现原理
RocketMQ 通过以下机制实现局部有序:
- 队列选择器:将相同业务 ID 的消息发送到同一个 Queue
- 顺序消费模式:确保同一 Queue 的消息被顺序消费
- 单线程消费:每个 Queue 在同一时刻只被一个线程消费
顺序消息的限制和注意事项
使用顺序消息前,需要了解以下限制:
- 性能影响:顺序消费是单线程的,吞吐量会显著降低
- 故障影响:某个消息处理失败会阻塞整个队列
- 扩容限制:增加消费者实例不能提高单个队列的消费速度
- Broker 故障:主从切换可能短暂影响顺序性
生产环境实战案例
以电商订单系统为例,订单状态必须按照:创建 → 支付 → 发货 → 完成的顺序流转。
配置管理
java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@ConfigurationProperties(prefix = "rocketmq.order")
@Data
@Component
public class OrderMessageConfig {
private int sendTimeout = 3000;
private int retryTimes = 2;
private int consumeThreadNumber = 1;
private int maxReconsumeTimes = 3;
private String nameServer = "localhost:9876";
private String producerGroup = "order-producer-group";
private String consumerGroup = "order-consumer-group";
private String topic = "ORDER_STATUS_TOPIC";
private int pullBatchSize = 1;
private int pullInterval = 0;
}
核心配置
java
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.List;
@Configuration
public class RocketMQOrderConfig {
@Resource
private OrderMessageConfig config;
@Bean
public RocketMQTemplate orderRocketMQTemplate() {
RocketMQTemplate template = new RocketMQTemplate();
DefaultMQProducer producer = new DefaultMQProducer();
producer.setNamesrvAddr(config.getNameServer());
producer.setProducerGroup(config.getProducerGroup());
producer.setSendMsgTimeout(config.getSendTimeout());
producer.setRetryTimesWhenSendFailed(config.getRetryTimes());
producer.setRetryTimesWhenSendAsyncFailed(config.getRetryTimes());
producer.setCompressMsgBodyOverHowmuch(4096);
template.setProducer(producer);
return template;
}
}
异常定义
java
public class BusinessException extends RuntimeException {
private boolean critical = false;
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, boolean critical) {
super(message);
this.critical = critical;
}
public boolean isCritical() {
return critical;
}
}
生产者实现
java
import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Component
@Slf4j
public class OrderMessageProducer {
@Resource
private RocketMQTemplate rocketMQTemplate;
@Resource
private FailedMessageRepository failedMessageRepository;
@Resource
private OrderMessageMetrics metrics;
@Resource
private OrderMessageConfig config;
/**
* 发送顺序消息
*/
public void sendOrderMessage(OrderMessage orderMessage) {
// 参数校验
Assert.notNull(orderMessage, "订单消息不能为空");
Assert.hasText(orderMessage.getOrderId(), "订单ID不能为空");
Assert.notNull(orderMessage.getStatus(), "订单状态不能为空");
// 生成消息ID
if (StringUtils.isEmpty(orderMessage.getMsgId())) {
orderMessage.setMsgId(UUID.randomUUID().toString());
}
// 设置操作时间
if (orderMessage.getOperateTime() == null) {
orderMessage.setOperateTime(LocalDateTime.now());
}
long startTime = System.currentTimeMillis();
try {
SendResult sendResult = rocketMQTemplate.syncSendOrderly(
config.getTopic(),
orderMessage,
orderMessage.getOrderId()
);
log.info("订单消息发送成功, orderId: {}, msgId: {}, queueId: {}",
orderMessage.getOrderId(),
sendResult.getMsgId(),
sendResult.getMessageQueue().getQueueId());
// 记录发送成功指标
metrics.recordMessageSend(orderMessage.getOrderId(), true);
metrics.recordSendLatency(System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("订单消息发送失败, orderId: {}", orderMessage.getOrderId(), e);
metrics.recordMessageSend(orderMessage.getOrderId(), false);
handleSendFailure(orderMessage);
}
}
/**
* 批量发送顺序消息(性能优化)
*/
public void sendBatchOrderMessage(List<OrderMessage> messages) {
// 按订单ID分组
Map<String, List<OrderMessage>> groupedMessages = messages.stream()
.collect(Collectors.groupingBy(OrderMessage::getOrderId));
groupedMessages.forEach((orderId, orderMessages) -> {
try {
// 同一订单的消息作为一批发送
rocketMQTemplate.syncSendOrderly(
config.getTopic(),
orderMessages,
orderId
);
metrics.recordBatchSend(orderId, orderMessages.size(), true);
} catch (Exception e) {
log.error("批量发送失败, orderId: {}, size: {}",
orderId, orderMessages.size(), e);
metrics.recordBatchSend(orderId, orderMessages.size(), false);
orderMessages.forEach(this::handleSendFailure);
}
});
}
private void handleSendFailure(OrderMessage orderMessage) {
try {
failedMessageRepository.save(FailedMessage.builder()
.messageId(orderMessage.getMsgId())
.orderId(orderMessage.getOrderId())
.topic(config.getTopic())
.content(JSON.toJSONString(orderMessage))
.retryCount(0)
.status("FAILED")
.createTime(LocalDateTime.now())
.build());
} catch (Exception e) {
log.error("保存失败消息异常, orderId: {}", orderMessage.getOrderId(), e);
}
}
}
消费者实现
java
import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
@RocketMQMessageListener(
topic = "${rocketmq.order.topic}",
consumerGroup = "${rocketmq.order.consumer-group}",
consumeMode = ConsumeMode.ORDERLY,
maxReconsumeTimes = 3,
consumeThreadNumber = 1
)
public class OrderMessageConsumer implements RocketMQListener<OrderMessage> {
@Resource
private OrderService orderService;
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private OrderMessageMetrics metrics;
@Resource
private OrderMessageErrorHandler errorHandler;
@Override
public void onMessage(OrderMessage message) {
long startTime = System.currentTimeMillis();
String orderId = message.getOrderId();
try {
// 幂等性检查
if (isMessageProcessed(message)) {
log.info("消息已处理,跳过, orderId: {}, msgId: {}",
orderId, message.getMsgId());
return;
}
// 处理订单状态变更
processOrderStatus(message);
// 记录处理成功
markMessageProcessed(message);
// 记录处理指标
metrics.recordOrderProcessingTime(orderId,
System.currentTimeMillis() - startTime);
metrics.recordConsumeSuccess(orderId);
} catch (BusinessException e) {
// 业务异常不重试
log.error("业务异常,不重试, orderId: {}, error: {}",
orderId, e.getMessage());
errorHandler.handleBusinessError(message, e);
} catch (Exception e) {
// 系统异常,抛出让RocketMQ重试
log.error("订单消息处理失败, orderId: {}", orderId, e);
metrics.recordConsumeFailure(orderId);
throw new RuntimeException("消息处理失败", e);
}
}
private void processOrderStatus(OrderMessage message) {
String orderId = message.getOrderId();
// 获取当前订单状态
OrderStatusEnum currentStatus = orderService.getOrderStatus(orderId);
OrderStatusEnum targetStatus = message.getStatus();
// 状态机验证
if (!isValidStatusTransition(currentStatus, targetStatus)) {
throw new BusinessException(String.format(
"非法状态转换, orderId: %s, current: %s, target: %s",
orderId, currentStatus, targetStatus));
}
// 更新订单状态
orderService.updateOrderStatus(orderId, targetStatus, message.getOperator());
log.info("订单状态更新成功, orderId: {}, {} -> {}",
orderId, currentStatus, targetStatus);
}
private boolean isValidStatusTransition(OrderStatusEnum current,
OrderStatusEnum target) {
// 状态转换规则
Map<OrderStatusEnum, Set<OrderStatusEnum>> validTransitions = Map.of(
OrderStatusEnum.CREATED,
Set.of(OrderStatusEnum.PAID, OrderStatusEnum.CANCELLED),
OrderStatusEnum.PAID,
Set.of(OrderStatusEnum.SHIPPED, OrderStatusEnum.REFUNDED),
OrderStatusEnum.SHIPPED,
Set.of(OrderStatusEnum.COMPLETED, OrderStatusEnum.RETURNED),
OrderStatusEnum.COMPLETED,
Set.of(OrderStatusEnum.RETURNED),
OrderStatusEnum.CANCELLED,
Set.of(),
OrderStatusEnum.REFUNDED,
Set.of(),
OrderStatusEnum.RETURNED,
Set.of()
);
return validTransitions.getOrDefault(current, Set.of()).contains(target);
}
private boolean isMessageProcessed(OrderMessage message) {
String key = String.format("msg:processed:%s:%s",
message.getOrderId(), message.getMsgId());
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
private void markMessageProcessed(OrderMessage message) {
String key = String.format("msg:processed:%s:%s",
message.getOrderId(), message.getMsgId());
redisTemplate.opsForValue().set(key, "1", 7, TimeUnit.DAYS);
}
}
错误处理器
java
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
@Component
@Slf4j
public class OrderMessageErrorHandler {
@Resource
private DeadLetterRepository deadLetterRepository;
@Resource
private NotificationService notificationService;
/**
* 处理业务异常
*/
public void handleBusinessError(OrderMessage message, BusinessException e) {
saveToDeadLetter(message, e, "BUSINESS_ERROR");
// 发送告警
if (e.isCritical()) {
notificationService.sendAlert(
"订单消息业务异常",
String.format("订单ID: %s, 错误: %s",
message.getOrderId(), e.getMessage())
);
}
}
/**
* 处理系统异常(带重试)
*/
@Retryable(value = Exception.class, maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2))
public void handleSystemError(OrderMessage message, Exception e) {
if (e instanceof DataAccessException) {
// 数据库异常,可能需要重试
log.warn("数据库异常,准备重试, orderId: {}", message.getOrderId());
throw (DataAccessException) e;
}
// 其他异常记录后重试
log.error("系统异常, orderId: {}", message.getOrderId(), e);
throw new RuntimeException("系统异常", e);
}
private void saveToDeadLetter(OrderMessage message, Exception e, String errorType) {
try {
deadLetterRepository.save(DeadLetter.builder()
.messageId(message.getMsgId())
.orderId(message.getOrderId())
.topic("ORDER_STATUS_TOPIC")
.content(JSON.toJSONString(message))
.errorType(errorType)
.errorMessage(e.getMessage())
.errorStack(getStackTrace(e))
.processed(false)
.createTime(LocalDateTime.now())
.build());
} catch (Exception ex) {
log.error("保存死信消息失败, orderId: {}", message.getOrderId(), ex);
}
}
private String getStackTrace(Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
}
消息实体定义
java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息ID(用于幂等)
*/
private String msgId;
/**
* 订单ID
*/
private String orderId;
/**
* 订单状态
*/
private OrderStatusEnum status;
/**
* 操作时间
*/
private LocalDateTime operateTime;
/**
* 操作人
*/
private String operator;
/**
* 扩展信息
*/
private Map<String, Object> extInfo;
}
public enum OrderStatusEnum {
CREATED("已创建"),
PAID("已支付"),
SHIPPED("已发货"),
COMPLETED("已完成"),
CANCELLED("已取消"),
REFUNDED("已退款"),
RETURNED("已退货");
private final String desc;
OrderStatusEnum(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
@Entity
@Table(name = "failed_message")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FailedMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String messageId;
private String orderId;
private String topic;
@Column(columnDefinition = "TEXT")
private String content;
private Integer retryCount;
private String status;
private LocalDateTime createTime;
private LocalDateTime lastRetryTime;
}
@Entity
@Table(name = "dead_letter")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeadLetter {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String messageId;
private String orderId;
private String topic;
@Column(columnDefinition = "TEXT")
private String content;
private String errorType;
private String errorMessage;
@Column(columnDefinition = "TEXT")
private String errorStack;
private Boolean processed;
private LocalDateTime createTime;
private LocalDateTime processTime;
}
Repository 接口定义
java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface FailedMessageRepository extends JpaRepository<FailedMessage, Long> {
List<FailedMessage> findByStatusAndRetryCountLessThan(String status, int maxRetry);
}
@Repository
public interface DeadLetterRepository extends JpaRepository<DeadLetter, Long> {
}
服务接口定义
java
public interface OrderService {
OrderStatusEnum getOrderStatus(String orderId);
void updateOrderStatus(String orderId, OrderStatusEnum status, String operator);
}
public interface NotificationService {
void sendAlert(String title, String content);
}
监控与指标
java
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import org.apache.rocketmq.common.admin.ConsumeStats;
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Component
@Slf4j
public class OrderMessageMetrics {
private final MeterRegistry registry;
private DefaultMQAdminExt mqAdminExt;
private final AtomicLong maxQueueDepth = new AtomicLong(0);
@Resource
private OrderMessageConfig config;
public OrderMessageMetrics(MeterRegistry registry) {
this.registry = registry;
}
@PostConstruct
public void init() {
try {
mqAdminExt = new DefaultMQAdminExt();
mqAdminExt.setNamesrvAddr(config.getNameServer());
mqAdminExt.start();
} catch (Exception e) {
log.error("初始化MQAdmin失败", e);
}
}
@PreDestroy
public void destroy() {
if (mqAdminExt != null) {
mqAdminExt.shutdown();
}
}
/**
* 记录消息发送
*/
public void recordMessageSend(String orderId, boolean success) {
registry.counter("order.message.send",
"orderId", orderId,
"result", success ? "success" : "fail"
).increment();
}
/**
* 记录发送延迟
*/
public void recordSendLatency(long latency) {
registry.timer("order.message.send.latency")
.record(latency, TimeUnit.MILLISECONDS);
}
/**
* 记录批量发送
*/
public void recordBatchSend(String orderId, int batchSize, boolean success) {
registry.counter("order.message.batch.send",
"orderId", orderId,
"batchSize", String.valueOf(batchSize),
"result", success ? "success" : "fail"
).increment();
}
/**
* 记录消费成功
*/
public void recordConsumeSuccess(String orderId) {
registry.counter("order.message.consume",
"orderId", orderId,
"result", "success"
).increment();
}
/**
* 记录消费失败
*/
public void recordConsumeFailure(String orderId) {
registry.counter("order.message.consume",
"orderId", orderId,
"result", "fail"
).increment();
}
/**
* 记录订单处理时间
*/
public void recordOrderProcessingTime(String orderId, long duration) {
registry.timer("order.processing.time",
Tags.of("orderId", orderId))
.record(duration, TimeUnit.MILLISECONDS);
// 处理时间过长告警
if (duration > 5000) {
log.warn("订单处理时间过长, orderId: {}, duration: {}ms",
orderId, duration);
}
}
/**
* 记录队列深度
*/
@Scheduled(fixedDelay = 60000) // 每分钟采集一次
public void recordQueueDepth() {
if (mqAdminExt == null) {
return;
}
try {
ConsumeStats consumeStats = mqAdminExt.examineConsumeStats(
config.getConsumerGroup());
consumeStats.getOffsetTable().forEach((queue, offset) -> {
long queueDepth = offset.getBrokerOffset() - offset.getConsumerOffset();
registry.gauge("rocketmq.queue.depth",
Tags.of("topic", queue.getTopic(),
"queue", String.valueOf(queue.getQueueId())),
Math.abs(queueDepth));
// 更新最大队列深度
maxQueueDepth.updateAndGet(current -> Math.max(current, Math.abs(queueDepth)));
// 队列积压告警
if (queueDepth > 1000) {
log.warn("队列积压严重, topic: {}, queueId: {}, depth: {}",
queue.getTopic(), queue.getQueueId(), queueDepth);
}
});
} catch (Exception e) {
log.error("获取队列深度失败", e);
}
}
public long getMaxQueueDepth() {
return maxQueueDepth.get();
}
}
故障恢复机制
java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import com.alibaba.fastjson.JSON;
@Component
@Slf4j
public class OrderMessageRecovery {
@Resource
private FailedMessageRepository failedMessageRepository;
@Resource
private DeadLetterRepository deadLetterRepository;
@Resource
private OrderMessageProducer producer;
@Resource
private OrderMessageMetrics metrics;
/**
* 定时恢复失败消息
*/
@Scheduled(fixedDelay = 300000) // 5分钟执行一次
public void recoverFailedMessages() {
List<FailedMessage> failedMessages = failedMessageRepository
.findByStatusAndRetryCountLessThan("FAILED", 3);
log.info("开始恢复失败消息, count: {}", failedMessages.size());
for (FailedMessage failed : failedMessages) {
try {
OrderMessage orderMessage = JSON.parseObject(
failed.getContent(), OrderMessage.class);
// 重新生成消息ID避免重复
orderMessage.setMsgId(UUID.randomUUID().toString());
// 重新发送
producer.sendOrderMessage(orderMessage);
// 更新状态
failed.setStatus("RECOVERED");
failed.setLastRetryTime(LocalDateTime.now());
failedMessageRepository.save(failed);
log.info("消息恢复成功, orderId: {}", orderMessage.getOrderId());
} catch (Exception e) {
failed.setRetryCount(failed.getRetryCount() + 1);
failed.setLastRetryTime(LocalDateTime.now());
if (failed.getRetryCount() >= 3) {
failed.setStatus("DEAD");
log.error("消息恢复失败超过最大次数, orderId: {}",
failed.getOrderId());
}
failedMessageRepository.save(failed);
}
}
}
/**
* 手动重试死信消息
*/
public void retryDeadLetter(Long deadLetterId) {
DeadLetter deadLetter = deadLetterRepository.findById(deadLetterId)
.orElseThrow(() -> new BusinessException("死信消息不存在"));
try {
OrderMessage orderMessage = JSON.parseObject(
deadLetter.getContent(), OrderMessage.class);
// 重新生成消息ID
orderMessage.setMsgId(UUID.randomUUID().toString());
// 重新发送
producer.sendOrderMessage(orderMessage);
// 标记已处理
deadLetter.setProcessed(true);
deadLetter.setProcessTime(LocalDateTime.now());
deadLetterRepository.save(deadLetter);
log.info("死信消息重试成功, id: {}, orderId: {}",
deadLetterId, orderMessage.getOrderId());
} catch (Exception e) {
log.error("死信消息重试失败, id: {}", deadLetterId, e);
throw new BusinessException("死信消息重试失败: " + e.getMessage());
}
}
}
消息追踪
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Aspect
@Component
public class MessageTraceAspect {
@Around("@annotation(MessageTrace)")
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
String traceId = MDC.get("traceId");
if (traceId == null) {
traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
}
try {
return joinPoint.proceed();
} finally {
MDC.remove("traceId");
}
}
}
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageTrace {
}
测试用例
java
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SpringBootTest
class OrderMessageTest {
@MockBean
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderMessageProducer producer;
@Autowired
private OrderMessageConsumer consumer;
@MockBean
private OrderService orderService;
@Test
void testSendOrderMessage() {
// 准备测试数据
OrderMessage message = OrderMessage.builder()
.msgId(UUID.randomUUID().toString())
.orderId("ORDER_001")
.status(OrderStatusEnum.PAID)
.operateTime(LocalDateTime.now())
.operator("system")
.build();
SendResult mockResult = new SendResult();
mockResult.setMsgId("test-msg-id");
MessageQueue mq = new MessageQueue("ORDER_STATUS_TOPIC", "broker-a", 0);
mockResult.setMessageQueue(mq);
when(rocketMQTemplate.syncSendOrderly(anyString(), any(), anyString()))
.thenReturn(mockResult);
// 执行测试
producer.sendOrderMessage(message);
// 验证调用
ArgumentCaptor<String> topicCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<OrderMessage> messageCaptor = ArgumentCaptor.forClass(OrderMessage.class);
ArgumentCaptor<String> orderIdCaptor = ArgumentCaptor.forClass(String.class);
verify(rocketMQTemplate).syncSendOrderly(
topicCaptor.capture(),
messageCaptor.capture(),
orderIdCaptor.capture()
);
assertEquals("ORDER_STATUS_TOPIC", topicCaptor.getValue());
assertEquals("ORDER_001", orderIdCaptor.getValue());
assertEquals(OrderStatusEnum.PAID, messageCaptor.getValue().getStatus());
}
@Test
void testConsumeOrderMessage() {
// 准备测试数据
OrderMessage message = OrderMessage.builder()
.msgId("test-msg-001")
.orderId("ORDER_001")
.status(OrderStatusEnum.PAID)
.operateTime(LocalDateTime.now())
.operator("system")
.build();
// Mock订单服务
when(orderService.getOrderStatus("ORDER_001"))
.thenReturn(OrderStatusEnum.CREATED);
// 执行消费
consumer.onMessage(message);
// 验证状态更新
verify(orderService).updateOrderStatus(
eq("ORDER_001"),
eq(OrderStatusEnum.PAID),
eq("system")
);
}
@Test
void testInvalidStatusTransition() {
// 测试非法状态转换
OrderMessage message = OrderMessage.builder()
.msgId("test-msg-002")
.orderId("ORDER_002")
.status(OrderStatusEnum.SHIPPED)
.operateTime(LocalDateTime.now())
.operator("system")
.build();
// Mock当前状态为已取消
when(orderService.getOrderStatus("ORDER_002"))
.thenReturn(OrderStatusEnum.CANCELLED);
// 执行消费,预期抛出业务异常
assertThrows(RuntimeException.class, () -> {
consumer.onMessage(message);
});
// 验证状态未更新
verify(orderService, never()).updateOrderStatus(any(), any(), any());
}
@Test
void testMessageIdempotency() {
// 测试消息幂等性
OrderMessage message = OrderMessage.builder()
.msgId("test-msg-003")
.orderId("ORDER_003")
.status(OrderStatusEnum.PAID)
.operateTime(LocalDateTime.now())
.operator("system")
.build();
when(orderService.getOrderStatus("ORDER_003"))
.thenReturn(OrderStatusEnum.CREATED);
// 第一次消费
consumer.onMessage(message);
verify(orderService, times(1)).updateOrderStatus(any(), any(), any());
// 第二次消费(应该被幂等性检查拦截)
consumer.onMessage(message);
verify(orderService, times(1)).updateOrderStatus(any(), any(), any());
}
@Test
void testConcurrentOrderMessages() {
// 测试并发场景下的顺序性
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < 100; i++) {
final int index = i;
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
OrderMessage message = OrderMessage.builder()
.msgId("msg-" + index)
.orderId("ORDER_001") // 同一订单
.status(OrderStatusEnum.values()[index % 3])
.build();
producer.sendOrderMessage(message);
});
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 验证消息都进入了同一个队列
verify(rocketMQTemplate, times(100))
.syncSendOrderly(anyString(), any(), eq("ORDER_001"));
}
}
性能优化配置
java
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class RocketMQPoolConfig {
@Resource
private OrderMessageConfig config;
@Bean
public GenericObjectPool<DefaultMQProducer> producerPool() {
GenericObjectPoolConfig<DefaultMQProducer> poolConfig =
new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(10);
poolConfig.setMaxIdle(5);
poolConfig.setMinIdle(2);
poolConfig.setMaxWaitMillis(3000);
poolConfig.setTestOnBorrow(true);
return new GenericObjectPool<>(new ProducerFactory(config), poolConfig);
}
@Bean
public DefaultMQPushConsumer orderConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setNamesrvAddr(config.getNameServer());
consumer.setConsumerGroup(config.getConsumerGroup());
// 顺序消费的性能优化配置
consumer.setConsumeThreadMin(1);
consumer.setConsumeThreadMax(1);
consumer.setConsumeMessageBatchMaxSize(1);
consumer.setPullBatchSize(config.getPullBatchSize());
consumer.setPullInterval(config.getPullInterval());
consumer.setConsumeTimeout(15);
return consumer;
}
}
// ProducerFactory 实现
class ProducerFactory extends BasePooledObjectFactory<DefaultMQProducer> {
private final OrderMessageConfig config;
public ProducerFactory(OrderMessageConfig config) {
this.config = config;
}
@Override
public DefaultMQProducer create() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer();
producer.setNamesrvAddr(config.getNameServer());
producer.setProducerGroup(config.getProducerGroup());
producer.setSendMsgTimeout(config.getSendTimeout());
producer.start();
return producer;
}
@Override
public PooledObject<DefaultMQProducer> wrap(DefaultMQProducer producer) {
return new DefaultPooledObject<>(producer);
}
@Override
public void destroyObject(PooledObject<DefaultMQProducer> p) throws Exception {
p.getObject().shutdown();
}
}
健康检查
java
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import javax.annotation.Resource;
@RestController
@RequestMapping("/actuator/health")
public class RocketMQHealthIndicator implements HealthIndicator {
@Resource
private OrderMessageMetrics metrics;
@Override
public Health health() {
try {
// 检查队列深度
long maxQueueDepth = metrics.getMaxQueueDepth();
if (maxQueueDepth > 10000) {
return Health.down()
.withDetail("queueDepth", maxQueueDepth)
.withDetail("status", "队列积压严重")
.build();
}
return Health.up()
.withDetail("queueDepth", maxQueueDepth)
.withDetail("status", "正常")
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
性能测试数据参考
基于 4 核 8G 的 RocketMQ 集群测试结果:
模式 | TPS | 延迟(P99) | CPU 使用率 | 内存使用 |
---|---|---|---|---|
普通模式 | 10000 | 10ms | 60% | 2G |
顺序模式(单队列) | 1000 | 15ms | 30% | 1G |
顺序模式(16 队列) | 8000 | 20ms | 70% | 2.5G |
顺序模式(32 队列) | 12000 | 25ms | 85% | 3G |
故障处理流程

最佳使用建议
1. 队列数量设置
根据业务量合理设置队列数量。队列数量越多,并发度越高,但管理复杂度也会增加。建议初始设置为 16 个队列,根据实际情况调整。
2. 消息大小控制
顺序消息建议控制在 4KB 以内,避免大消息影响处理速度。如果需要传输大数据,可以将数据存储到 OSS 或数据库,消息中只传递引用。
3. 超时设置
合理设置消费超时时间,避免单个消息阻塞整个队列。建议设置为业务处理时间的 2-3 倍。
4. 监控告警
建立完善的监控体系,重点关注:
- 队列积压深度
- 消息处理延迟
- 失败消息数量
- 消费者健康状态
5. 消息预取优化
对于顺序消息,可以通过调整预取数量来优化性能:
java
consumer.setPullBatchSize(1); // 顺序消费建议设为1
consumer.setPullInterval(0); // 立即拉取下一条
6. 故障隔离
不同业务使用不同的 Topic,避免相互影响。关键业务可以部署独立的 RocketMQ 集群。
常见问题处理
1. 消息积压处理
java
@Component
@Slf4j
public class MessageBacklogHandler {
@Resource
private OrderMessageMetrics metrics;
@Resource
private NotificationService notificationService;
@Scheduled(fixedDelay = 300000)
public void checkBacklog() {
// 检查队列积压情况
long backlog = metrics.getMaxQueueDepth();
if (backlog > 10000) {
// 动态增加消费者实例
scaleUpConsumers();
// 发送告警
notificationService.sendAlert("队列积压告警",
"队列积压严重: " + backlog);
}
}
private void scaleUpConsumers() {
// 实现动态扩容逻辑
log.info("触发消费者动态扩容");
}
}
2. 消费者宕机恢复
java
@Component
@Slf4j
public class ConsumerHealthChecker {
@Resource
private FailedMessageRepository failedMessageRepository;
@Resource
private OrderMessageProducer producer;
@EventListener(ApplicationReadyEvent.class)
public void startHealthCheck() {
// 启动时检查未完成的消息
List<FailedMessage> unfinished = failedMessageRepository
.findByStatusAndRetryCountLessThan("PROCESSING", 3);
for (FailedMessage msg : unfinished) {
// 重新投递消息
try {
OrderMessage orderMessage = JSON.parseObject(
msg.getContent(), OrderMessage.class);
producer.sendOrderMessage(orderMessage);
log.info("重新投递未完成消息: {}", msg.getOrderId());
} catch (Exception e) {
log.error("重新投递失败: {}", msg.getOrderId(), e);
}
}
}
}
配置文件示例
yaml
rocketmq:
order:
name-server: localhost:9876
producer-group: order-producer-group
consumer-group: order-consumer-group
topic: ORDER_STATUS_TOPIC
send-timeout: 3000
retry-times: 2
consume-thread-number: 1
max-reconsume-times: 3
pull-batch-size: 1
pull-interval: 0
spring:
redis:
host: localhost
port: 6379
timeout: 3000
datasource:
url: jdbc:mysql://localhost:3306/order_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
management:
metrics:
export:
prometheus:
enabled: true
endpoints:
web:
exposure:
include: health,metrics,prometheus
附录:完整项目结构
css
project-root/
├── src/main/java/
│ ├── config/
│ │ ├── OrderMessageConfig.java
│ │ ├── RocketMQOrderConfig.java
│ │ └── RocketMQPoolConfig.java
│ ├── producer/
│ │ └── OrderMessageProducer.java
│ ├── consumer/
│ │ └── OrderMessageConsumer.java
│ ├── entity/
│ │ ├── OrderMessage.java
│ │ ├── FailedMessage.java
│ │ └── DeadLetter.java
│ ├── handler/
│ │ └── OrderMessageErrorHandler.java
│ ├── metrics/
│ │ └── OrderMessageMetrics.java
│ ├── recovery/
│ │ └── OrderMessageRecovery.java
│ ├── aspect/
│ │ └── MessageTraceAspect.java
│ ├── repository/
│ │ ├── FailedMessageRepository.java
│ │ └── DeadLetterRepository.java
│ ├── service/
│ │ ├── OrderService.java
│ │ └── NotificationService.java
│ ├── health/
│ │ └── RocketMQHealthIndicator.java
│ ├── exception/
│ │ └── BusinessException.java
│ └── annotation/
│ └── MessageTrace.java
├── src/main/resources/
│ ├── application.yml
│ └── logback-spring.xml
└── src/test/java/
└── OrderMessageTest.java
日志配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATH" value="./logs"/>
<property name="LOG_FILE" value="order-message"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache.rocketmq" level="WARN"/>
<logger name="com.example.order" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
Maven 依赖
xml
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- RocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- Pool -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
数据库表结构
sql
-- 失败消息表
CREATE TABLE `failed_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`message_id` varchar(64) NOT NULL COMMENT '消息ID',
`order_id` varchar(64) NOT NULL COMMENT '订单ID',
`topic` varchar(128) NOT NULL COMMENT '主题',
`content` text NOT NULL COMMENT '消息内容',
`retry_count` int(11) DEFAULT '0' COMMENT '重试次数',
`status` varchar(32) NOT NULL COMMENT '状态',
`create_time` datetime NOT NULL COMMENT '创建时间',
`last_retry_time` datetime DEFAULT NULL COMMENT '最后重试时间',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_status_retry` (`status`,`retry_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='失败消息表';
-- 死信消息表
CREATE TABLE `dead_letter` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`message_id` varchar(64) NOT NULL COMMENT '消息ID',
`order_id` varchar(64) NOT NULL COMMENT '订单ID',
`topic` varchar(128) NOT NULL COMMENT '主题',
`content` text NOT NULL COMMENT '消息内容',
`error_type` varchar(64) NOT NULL COMMENT '错误类型',
`error_message` varchar(512) DEFAULT NULL COMMENT '错误信息',
`error_stack` text COMMENT '错误堆栈',
`processed` tinyint(1) DEFAULT '0' COMMENT '是否已处理',
`create_time` datetime NOT NULL COMMENT '创建时间',
`process_time` datetime DEFAULT NULL COMMENT '处理时间',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_processed` (`processed`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='死信消息表';
总结
关键点 | 实现方式 | 注意事项 |
---|---|---|
消息路由 | 使用业务 ID 作为路由键 | 确保哈希算法的稳定性 |
顺序保证 | ConsumeMode.ORDERLY + 单线程 | 牺牲性能换取顺序性 |
幂等处理 | Redis 记录消息 ID | 设置合理的过期时间 |
状态验证 | 状态机模式 | 防止非法状态跳转 |
性能优化 | 合理设置队列数 | 根据业务量动态调整 |
故障处理 | 重试+补偿+死信 | 避免消息丢失 |
监控指标 | Micrometer + 定时采集 | 及时发现问题 |