RocketMQ 消息顺序性:从原理到实战的完整解决方案

RocketMQ 默认情况下不保证全局消息顺序,但在特定场景下可以实现局部有序。本文将深入探讨 RocketMQ 的顺序消息机制,并提供生产环境的实战方案。

消息顺序性的本质

在分布式系统中,消息顺序性分为两个层面:

1. 全局有序

所有消息严格按照发送顺序被消费,整个 Topic 只有一个队列,性能极低。

2. 局部有序

相同业务标识的消息保持顺序,不同业务标识的消息可以并行处理。

RocketMQ 顺序消息的实现原理

RocketMQ 通过以下机制实现局部有序:

  1. 队列选择器:将相同业务 ID 的消息发送到同一个 Queue
  2. 顺序消费模式:确保同一 Queue 的消息被顺序消费
  3. 单线程消费:每个 Queue 在同一时刻只被一个线程消费

顺序消息的限制和注意事项

使用顺序消息前,需要了解以下限制:

  1. 性能影响:顺序消费是单线程的,吞吐量会显著降低
  2. 故障影响:某个消息处理失败会阻塞整个队列
  3. 扩容限制:增加消费者实例不能提高单个队列的消费速度
  4. 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 + 定时采集 及时发现问题
相关推荐
伍六星5 分钟前
更新Java的环境变量后VScode/cursor里面还是之前的环境变量
java·开发语言·vscode
风象南11 分钟前
SpringBoot实现简易直播
java·spring boot·后端
万能程序员-传康Kk20 分钟前
智能教育个性化学习平台-java
java·开发语言·学习
落笔画忧愁e29 分钟前
扣子Coze飞书多维表插件-列出全部数据表
java·服务器·飞书
鱼儿也有烦恼32 分钟前
Elasticsearch最新入门教程
java·elasticsearch·kibana
eternal__day42 分钟前
微服务架构下的服务注册与发现:Eureka 深度解析
java·spring cloud·微服务·eureka·架构·maven
一介草民丶1 小时前
Jenkins | Linux环境部署Jenkins与部署java项目
java·linux·jenkins
武子康1 小时前
Java-39 深入浅出 Spring - AOP切面增强 核心概念 通知类型 XML+注解方式 附代码
xml·java·大数据·开发语言·后端·spring
米粉03051 小时前
SpringBoot核心注解详解及3.0与2.0版本深度对比
java·spring boot·后端
ademen3 小时前
spring4第6课-bean之间的关系+bean的作用范围
java·spring