RabbitMQ四板斧,订单超时取消就该这么干!

25 阅读9分钟

《RabbitMQ四板斧,订单超时取消就该这么干!》

我是小坏,今儿个咱不扯那么多花里胡哨的,直接捞干的讲。昨天说了缓存,今儿聊聊消息队列。RabbitMQ这玩意儿,说简单也简单,说难也难。关键是你得知道啥时候用,咋用。

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

一、别急着上技术,先想清楚场景

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

前两天有个老弟问我:“哥,订单30分钟不支付自动取消,咋实现?”

我问:“你现在咋弄的?” 他说:“定时任务,每分钟扫一遍表。” 我问:“多少订单?” 他说:“一天十几万吧。” 我说:“那你数据库不得被你扫秃噜皮了?”

定时任务扫表的毛病

  • 每分钟全表扫描,数据库压力大
  • 取消时间不精准(可能晚1分钟)
  • 订单多了根本扫不动

换成消息队列,咋弄

  • 用户下单 → 发个30分钟后到期的消息
  • RabbitMQ到点告诉你:“该取消了!”
  • 直接处理,不用扫表

二、RabbitMQ快速上手

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

2.1 先整明白四个概念

  1. 生产者:发消息的(比如下单服务)
  2. 消费者:收消息的(比如取消订单服务)
  3. 交换机:邮局,决定把信往哪儿送
  4. 队列:邮箱,消息在这儿等着

2.2 5分钟跑起来

第一步:加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

第二步:写配置

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    # 重点:开启消息确认
    publisher-confirms: true
    publisher-returns: true

第三步:发消息

@RestController
public class OrderController {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @PostMapping("/order")
    public String createOrder(@RequestBody Order order) {
        // 1. 保存订单到数据库
        orderService.save(order);
        
        // 2. 发个延迟消息,30分钟后取消
        rabbitTemplate.convertAndSend(
            "order.exchange",      // 交换机
            "order.cancel",        // 路由键
            order.getId(),         // 订单ID
            message -> {
                // 设置30分钟后过期
                message.getMessageProperties()
                       .setExpiration("1800000");  // 30分钟=1800000毫秒
                return message;
            }
        );
        
        return "下单成功,请30分钟内支付";
    }
}

三、四种交换机模式,别用错了

3.1 Direct(直连)模式

特点:一把钥匙开一把锁 场景:订单取消(指定队列)

@Configuration
public class DirectConfig {
    // 创建队列
    @Bean
    public Queue cancelQueue() {
        return new Queue("order.cancel.queue");
    }
    
    // 创建交换机
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange("order.exchange");
    }
    
    // 绑定:把队列绑到交换机上
    @Bean
    public Binding cancelBinding() {
        return BindingBuilder.bind(cancelQueue())
                .to(orderExchange())
                .with("order.cancel");  // 路由键
    }
}

3.2 Fanout(广播)模式

特点:大喇叭广播,谁都能听见 场景:订单创建成功,通知所有相关服务

@Configuration
public class FanoutConfig {
    // 三个队列:发短信、发邮件、更新统计
    @Bean public Queue smsQueue() { return new Queue("sms.queue"); }
    @Bean public Queue emailQueue() { return new Queue("email.queue"); }
    @Bean public Queue statsQueue() { return new Queue("stats.queue"); }
    
    // 广播交换机
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("order.success.exchange");
    }
    
    // 都绑到同一个交换机
    @Bean public Binding smsBinding() { /* ... */ }
    @Bean public Binding emailBinding() { /* ... */ }
    @Bean public Binding statsBinding() { /* ... */ }
}

3.3 Topic(主题)模式

特点:按话题订阅 场景:商品更新,不同服务关心不同商品

// 路由键规则:order.手机.创建
//             order.电脑.创建
//             order.手机.取消
@Configuration
public class TopicConfig {
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange("order.topic.exchange");
    }
    
    // 监听所有手机订单
    @Bean public Queue phoneQueue() { return new Queue("phone.queue"); }
    @Bean public Binding phoneBinding() {
        return BindingBuilder.bind(phoneQueue())
                .to(topicExchange())
                .with("order.手机.*");  // * 匹配一个词
    }
    
    // 监听所有创建订单
    @Bean public Queue createQueue() { return new Queue("create.queue"); }
    @Bean public Binding createBinding() {
        return BindingBuilder.bind(createQueue())
                .to(topicExchange())
                .with("order.*.创建");  // # 匹配多个词
    }
}

3.4 Headers模式(用的少)

特点:根据消息头匹配 场景:特殊需求,一般用不到

四、死信队列:订单取消的核心

4.1 啥是死信?

消息变成“死信”的三种情况:

  1. 消息被拒绝(并且不重新入队)
  2. 消息过期
  3. 队列满了

4.2 订单取消实战

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Configuration
public class DeadLetterConfig {
    
    // 1. 创建死信交换机(就是个普通交换机)
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange("dead.letter.exchange");
    }
    
    // 2. 创建死信队列
    @Bean
    public Queue deadLetterQueue() {
        return new Queue("dead.letter.queue");
    }
    
    // 3. 绑定死信队列
    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue())
                .to(deadLetterExchange())
                .with("dead.letter");
    }
    
    // 4. 创建订单队列,并指定死信交换机
    @Bean
    public Queue orderQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", "dead.letter.exchange"); // 死信交换机
        args.put("x-dead-letter-routing-key", "dead.letter");      // 死信路由键
        args.put("x-message-ttl", 1800000);                         // 30分钟过期
        return new Queue("order.queue", true, false, false, args);
    }
    
    // 5. 监听死信队列(处理过期订单)
    @Component
    @Slf4j
    public class OrderCancelConsumer {
        
        @RabbitListener(queues = "dead.letter.queue")
        public void cancelOrder(String orderId) {
            log.info("订单 {} 30分钟未支付,开始取消", orderId);
            
            // 1. 查询订单状态
            Order order = orderService.findById(orderId);
            if (order == null || !"待支付".equals(order.getStatus())) {
                log.warn("订单 {} 状态异常,无需取消", orderId);
                return;
            }
            
            // 2. 取消订单
            order.setStatus("已取消");
            order.setCancelTime(new Date());
            order.setCancelReason("超时未支付");
            orderService.update(order);
            
            // 3. 释放库存
            inventoryService.releaseStock(order.getProductId(), order.getQuantity());
            
            // 4. 通知用户
            notifyService.sendCancelNotice(order.getUserId(), orderId);
            
            log.info("订单 {} 取消完成", orderId);
        }
    }
}

五、消息可靠性:别把订单弄丢了

5.1 生产者确认(发出去要知道成没成)

spring:
  rabbitmq:
    # 开启确认回调
    publisher-confirms: true
    publisher-returns: true
@Component
@Slf4j
public class RabbitConfirmCallback implements RabbitTemplate.ConfirmCallback,
                                              RabbitTemplate.ReturnCallback {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.setMandatory(true);  // 消息无法路由时返回给生产者
    }
    
    // 消息是否成功到达交换机
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息到达交换机,消息ID:{}", correlationData.getId());
        } else {
            log.error("消息未到达交换机,原因:{},消息ID:{}", cause, correlationData.getId());
            // 这里可以重发或者记录到数据库
            resendMessage(correlationData);
        }
    }
    
    // 消息是否成功到达队列(路由失败时触发)
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText,
                                String exchange, String routingKey) {
        log.error("消息路由失败,交换机:{},路由键:{},消息:{}", 
                 exchange, routingKey, new String(message.getBody()));
        // 处理路由失败的消息
        handleRoutingFailure(message, exchange, routingKey);
    }
}

5.2 消费者确认(处理完要告诉RabbitMQ)

@Component
@Slf4j
public class OrderConsumer {
    
    // 手动确认模式
    @RabbitListener(queues = "order.queue", ackMode = "MANUAL")
    public void handleOrder(Message message, Channel channel,
                            @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        try {
            String orderId = new String(message.getBody());
            
            // 处理订单
            boolean success = processOrder(orderId);
            
            if (success) {
                // 处理成功,确认消息
                channel.basicAck(deliveryTag, false);
                log.info("订单处理成功:{}", orderId);
            } else {
                // 处理失败,拒绝消息(不重新入队)
                channel.basicNack(deliveryTag, false, false);
                log.error("订单处理失败:{}", orderId);
                
                // 记录到数据库,人工处理
                saveFailedMessage(orderId);
            }
        } catch (Exception e) {
            log.error("处理订单异常", e);
            try {
                // 发生异常,拒绝消息(重新入队)
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException ex) {
                log.error("拒绝消息失败", ex);
            }
        }
    }
    
    // 自动确认(小心使用)
    @RabbitListener(queues = "sms.queue", ackMode = "AUTO")
    public void handleSms(String phone) {
        // 这里如果抛异常,消息会重新入队
        // 如果是发短信,可能造成重复发送
        smsService.send(phone);
    }
}

六、实战:电商订单完整流程

6.1 下单流程

@Service
@Transactional
@Slf4j
public class OrderService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public OrderResult createOrder(OrderRequest request) {
        // 1. 扣减库存(预占库存)
        boolean stockSuccess = inventoryService.lockStock(
            request.getProductId(), 
            request.getQuantity()
        );
        
        if (!stockSuccess) {
            return OrderResult.fail("库存不足");
        }
        
        // 2. 创建订单
        Order order = new Order();
        order.setStatus("待支付");
        order.setCreateTime(new Date());
        // ... 其他字段
        order = orderRepository.save(order);
        
        try {
            // 3. 发送延迟消息(30分钟取消)
            rabbitTemplate.convertAndSend(
                "order.exchange",
                "order.create",
                order.getId(),
                message -> {
                    message.getMessageProperties()
                           .setExpiration("1800000");  // 30分钟
                    // 设置消息ID,用于追踪
                    message.getMessageProperties()
                           .setCorrelationId(order.getId());
                    return message;
                }
            );
            
            // 4. 发送创建成功通知(广播)
            rabbitTemplate.convertAndSend(
                "order.success.exchange",
                "",
                order.getId()  // 广播不需要路由键
            );
            
            return OrderResult.success(order);
            
        } catch (Exception e) {
            // 消息发送失败,回滚订单
            log.error("订单创建消息发送失败,回滚订单", e);
            throw new RuntimeException("订单创建失败", e);
        }
    }
}

6.2 支付成功处理

@Service
@Slf4j
public class PaymentService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Transactional
    public void handlePaymentSuccess(String orderId) {
        // 1. 更新订单状态
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("已支付");
        order.setPayTime(new Date());
        orderRepository.save(order);
        
        // 2. 扣减真实库存(预占转实际)
        inventoryService.deductStock(order.getProductId(), order.getQuantity());
        
        // 3. 取消之前的延迟消息(如果还没被消费)
        // 注意:RabbitMQ不支持直接取消延迟消息
        // 需要在消费者端做幂等性处理
        
        // 4. 发送支付成功通知
        rabbitTemplate.convertAndSend(
            "order.exchange",
            "order.pay.success",
            orderId
        );
        
        log.info("订单 {} 支付成功处理完成", orderId);
    }
}

七、常见问题与解决方案

问题1:消息重复消费

场景:网络问题导致消费者确认失败,消息重新入队 解决:消费者做幂等处理

@Service
public class OrderCancelService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @RabbitListener(queues = "dead.letter.queue")
    public void cancelOrder(String orderId) {
        // 使用Redis做幂等性控制
        String key = "order:cancel:" + orderId;
        
        // 使用setnx,只有第一次能设置成功
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(key, "processing", 5, TimeUnit.MINUTES);
        
        if (success != null && success) {
            // 第一次处理
            doCancelOrder(orderId);
        } else {
            // 已经处理过或在处理中
            log.warn("订单 {} 取消请求重复,忽略", orderId);
        }
    }
}

问题2:消息顺序问题

场景:先支付成功,后取消订单 解决:版本号或状态机

public class Order {
    @Version
    private Integer version;  // 乐观锁版本号
    
    public boolean canCancel() {
        // 只有待支付状态才能取消
        return "待支付".equals(this.status);
    }
    
    public boolean canPay() {
        // 只有待支付状态才能支付
        return "待支付".equals(this.status);
    }
}

问题3:队列积压

场景:消费者挂了,消息堆积 解决:监控+告警+扩容

# 监控RabbitMQ
management:
  endpoints:
    web:
      exposure:
        include: health,metrics
  metrics:
    export:
      prometheus:
        enabled: true

八、性能优化建议

8.1 连接池配置

spring:
  rabbitmq:
    # 连接池配置
    cache:
      channel:
        size: 25        # 通道缓存大小
      connection:
        mode: channel   # 连接模式
        size: 5         # 连接池大小

8.2 批量确认

channel.basicAck(deliveryTag, true);  // 批量确认

8.3 预取数量

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
        ConnectionFactory connectionFactory) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setPrefetchCount(10);  // 每次预取10条
    return factory;
}

九、今日要点总结

  1. 延迟消息用死信队列:订单取消、优惠券过期
  2. 广播用Fanout:通知多个服务
  3. 精准路由用Direct:指定到具体队列
  4. 灵活订阅用Topic:按规则订阅
  5. 消息一定要可靠:确认机制+幂等处理
  6. 监控不能少:队列长度、消费速度

十、思考题

场景:双十一大促,订单量暴涨100倍

  1. 消息队列如何扩容?
  2. 消费者挂了怎么办?
  3. 如何保证消息不丢失?
  4. 如何快速处理积压消息?

评论区聊聊你的方案,明儿咱们讲搜索。


明天预告:《SpringBoot+ES:打造毫秒级搜索》

今日福利:关注后回复“RabbitMQ”,获取完整订单系统源码。


公众号运营小贴士:

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

🏷️ 标签:#RabbitMQ #消息队列 #订单系统

💡 互动

  1. 你用过消息队列解决过啥问题?
  2. 投票:你们公司用RabbitMQ还是Kafka?
  3. 留言提问,明天文章解答

🎁 福利

  1. 留言区抽3人送《RabbitMQ实战》
  2. 转发截图送配置文件模板

👥 进群: 扫码加技术群,获取完整源码

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。