在高并发分布式环境下,实现下单5分钟未支付就自动取消订单

49 阅读6分钟

你需要使用 Spring Cloud Stream + RabbitMQ 实现死信队列功能,核心需求是订单创建后 5 分钟内未支付就自动取消。这个场景非常适合用 RabbitMQ 的 TTL(消息过期时间)+ 死信队列机制来实现。

实现思路

  1. 普通队列:接收订单创建消息,设置 5 分钟(300000 毫秒)的消息过期时间
  2. 死信交换机 / 队列:当普通队列的消息过期后,会自动转发到死信队列
  3. 生产者:订单创建时发送消息到普通队列
  4. 消费者:监听死信队列,接收过期消息并执行订单取消逻辑

完整实现代码

1. 首先添加依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <!-- 稳定版本 --> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>stream-rabbit-dlq-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>stream-rabbit-dlq-demo</name> <properties> <java.version>1.8</java.version> <spring-cloud.version>2021.0.9</spring-cloud.version> </properties> <dependencies> <!-- Spring Boot 基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Cloud Stream RabbitMQ 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <!-- 工具类依赖 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>

2. 配置文件(application.yml)

spring: # RabbitMQ 连接配置 rabbitmq: host: localhost # 你的 RabbitMQ 地址 port: 5672 # 默认端口 username: guest # 默认用户名 password: guest # 默认密码 virtual-host: / # Spring Cloud Stream 配置 cloud: stream: # 绑定器配置(RabbitMQ) rabbit: bindings: # 普通队列的生产者绑定配置 order-producer-out-0: producer: # 开启消息确认 confirm-type: correlated # 消息过期时间:5分钟 = 300000毫秒 ttl: 300000 # 指定死信交换机 dead-letter-exchange: order-dlq-exchange # 指定死信路由键 dead-letter-routing-key: order.dlq.key # 死信队列的消费者绑定配置 order-dlq-consumer-in-0: consumer: # 自动确认消息 auto-ack: true # 并发消费者数量 concurrency: 1 # 最大并发消费者数量 max-concurrency: 3 # 绑定配置(定义输入输出通道与交换机的绑定关系) bindings: # 生产者输出通道 order-producer-out-0: destination: order-exchange # 普通交换机名称 routing-key-expression: headers['routeKey'] # 路由键表达式 content-type: application/json # 消息格式 # 死信队列消费者输入通道 order-dlq-consumer-in-0: destination: order-dlq-exchange # 死信交换机名称 group: order-dlq-group # 消费者组(避免重复消费) routing-key: order.dlq.key # 死信路由键 content-type: application/json # 日志配置(方便查看执行流程) logging: level: org.springframework.cloud.stream: INFO com.example: DEBUG

3. 定义消息通道接口(StreamConfig.java)

package com.example.config; import org.springframework.cloud.stream.annotation.Input; import org.springframework.cloud.stream.annotation.Output; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; /** * 定义 Spring Cloud Stream 的输入输出通道 */ public interface StreamConfig { // 普通队列生产者输出通道名称 String ORDER_PRODUCER_OUT = "order-producer-out-0"; // 死信队列消费者输入通道名称 String ORDER_DLQ_CONSUMER_IN = "order-dlq-consumer-in-0"; /** * 订单生产者输出通道 */ @Output(ORDER_PRODUCER_OUT) MessageChannel orderProducerOut(); /** * 死信队列消费者输入通道 */ @Input(ORDER_DLQ_CONSUMER_IN) SubscribableChannel orderDlqConsumerIn(); }

4. 订单实体类(Order.java)

package com.example.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; /** * 订单实体类(必须实现 Serializable 接口,因为 RabbitMQ 消息需要序列化) */ @Data @NoArgsConstructor @AllArgsConstructor public class Order implements Serializable { private static final long serialVersionUID = 1L; private String orderId; // 订单ID(唯一标识) private String userId; // 用户ID private BigDecimal amount; // 订单金额 private String status; // 订单状态(PENDING:待支付,CANCELED:已取消,PAID:已支付) private Date createTime; // 创建时间 private Date expireTime; // 过期时间 }

5. 订单生产者(OrderProducer.java)

package com.example.producer; import com.example.config.StreamConfig; import com.example.entity.Order; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; import org.springframework.util.MimeTypeUtils; import java.math.BigDecimal; import java.util.Date; import java.util.UUID; /** * 订单生产者:发送订单消息到普通队列 */ @Component @EnableBinding(StreamConfig.class) // 绑定通道配置 @Slf4j public class OrderProducer { private final StreamConfig streamConfig; // 构造函数注入通道配置 public OrderProducer(StreamConfig streamConfig) { this.streamConfig = streamConfig; } /** * 创建订单并发送到队列 */ public String createOrder(String userId, BigDecimal amount) { // 1. 构建订单信息 String orderId = UUID.randomUUID().toString().replace("-", ""); Date createTime = new Date(); // 计算过期时间(当前时间 + 5分钟) Date expireTime = new Date(createTime.getTime() + 300000); Order order = new Order( orderId, userId, amount, "PENDING", // 初始状态:待支付 createTime, expireTime ); // 2. 构建消息(指定路由键) Message<Order> message = MessageBuilder .withPayload(order) .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON) .setHeader("routeKey", "order.normal.key") // 普通队列路由键 .build(); // 3. 发送消息到普通队列 boolean sendResult = streamConfig.orderProducerOut().send(message); if (sendResult) { log.info("订单创建成功并发送到队列!订单ID:{},创建时间:{},过期时间:{}", orderId, createTime, expireTime); return orderId; } else { log.error("订单消息发送失败!订单ID:{}", orderId); return null; } } }

6. 死信队列消费者(OrderDlqConsumer.java)

package com.example.consumer; import com.example.config.StreamConfig; import com.example.entity.Order; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; /** * 死信队列消费者:处理过期未支付的订单(自动取消) */ @Component @EnableBinding(StreamConfig.class) // 绑定通道配置 @Slf4j public class OrderDlqConsumer { /** * 监听死信队列,处理过期订单 */ @StreamListener(StreamConfig.ORDER_DLQ_CONSUMER_IN) // 监听死信输入通道 public void handleExpiredOrder(@Payload Order order) { try { log.info("收到过期未支付订单,开始自动取消!订单信息:{}", order); // TODO: 核心业务逻辑(根据实际需求实现) // 1. 检查订单当前状态(防止重复取消) if ("PENDING".equals(order.getStatus())) { // 2. 更新订单状态为 "已取消"(实际项目中需要操作数据库) order.setStatus("CANCELED"); // 3. 其他业务逻辑(如:恢复商品库存、发送取消通知给用户等) log.info("订单取消成功!订单ID:{},当前状态:{}", order.getOrderId(), order.getStatus()); } else { log.warn("订单状态异常,无需取消!订单ID:{},当前状态:{}", order.getOrderId(), order.getStatus()); } } catch (Exception e) { log.error("订单取消失败!订单ID:{},异常信息:{}", order.getOrderId(), e.getMessage(), e); // 实际项目中可以添加重试机制或告警通知 } } }

7. 测试接口(OrderController.java)

package com.example.controller; import com.example.producer.OrderProducer; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; /** * 测试接口:创建订单(用于触发整个流程) */ @RestController @RequestMapping("/orders") @Slf4j public class OrderController { private final OrderProducer orderProducer; public OrderController(OrderProducer orderProducer) { this.orderProducer = orderProducer; } /** * 创建测试订单 * 访问示例:http://localhost:8080/orders/create?userId=1001&amount=99.99 */ @GetMapping("/create") public String createOrder( @RequestParam String userId, @RequestParam BigDecimal amount) { String orderId = orderProducer.createOrder(userId, amount); if (orderId != null) { return "订单创建成功!订单ID:" + orderId + ",5分钟内未支付将自动取消"; } else { return "订单创建失败,请稍后重试"; } } }

8. 启动类(StreamRabbitDlqDemoApplication.java)

package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class StreamRabbitDlqDemoApplication { public static void main(String[] args) { SpringApplication.run(StreamRabbitDlqDemoApplication.class, args); } }

关键配置说明

  1. TTL 配置:通过 spring.cloud.stream.rabbit.bindings.order-producer-out-0.producer.ttl=300000 设置消息 5 分钟过期
  2. 死信交换机 / 路由键:通过 dead-letter-exchange 和 dead-letter-routing-key 指定死信目的地
  3. 消息序列化:订单实体类必须实现 Serializable 接口,否则消息无法在 RabbitMQ 中传输
  4. 消费者组:死信队列消费者配置了 group: order-dlq-group,避免多实例部署时重复消费

测试步骤

  1. 启动 RabbitMQ 服务(确保地址、端口、账号密码配置正确)
  2. 启动 Spring Boot 应用
  3. 访问测试接口创建订单:http://localhost:8080/orders/create?userId=1001&amount=99.99
  4. 查看日志,会看到 "订单创建成功并发送到队列" 的日志
  5. 等待 5 分钟后,会看到 "收到过期未支付订单,开始自动取消" 的日志

注意事项

  1. RabbitMQ 版本:建议使用 3.8+ 版本,确保死信队列功能正常
  2. 消息持久化:默认情况下,Spring Cloud Stream RabbitMQ Binder 会将队列和消息设置为持久化,确保应用重启后消息不丢失
  3. 订单状态一致性:实际项目中,取消订单时需要先检查订单当前状态,避免重复取消(比如用户在 5 分钟内支付了,但消息已经进入死信队列)
  4. 重试机制:可以为死信队列消费者添加重试配置,处理数据库异常等情况

总结

  1. 核心实现:利用 RabbitMQ 的 TTL 机制 + 死信队列,实现订单 5 分钟未支付自动取消
  2. 关键配置:普通队列设置 TTL 和死信目的地,死信队列消费者监听处理过期消息
  3. 优势:解耦订单创建和取消逻辑,无需定时任务轮询数据库,性能更优,实时性更好