Spring Boot + RabbitMQ實現訂單超時自動取消功能


場景:在京東下單,訂單創建成功,等待支付,一般會給30分鍾的時間,開始倒計時。如果在這段時間內
用戶沒有支付,則默認訂單取消。

如何訂單超時實現?
  • 定時任務
  • redission延時任務
  • rabbitmq死信隊列

本文將以rabbitmq死信隊列展開做講解,因為定時任務的方式,是有點問題的,原本業務系統希望10分鍾后,如果訂單未支付,就馬上取消訂單,並釋放商品庫存。但是一旦數據量大的話,就會加長獲取未支付訂單數據的時間,部分訂單就做不到10分鍾后取消了,可能是15分鍾,20分鍾之類的。這樣的話,庫存就無法及時得到釋放,也就會影響成單數。而使用rabbitmq死信隊列,在定義業務隊列時可以考慮指定一個死信交換機,並綁定一個死信隊列。當消息變成死信時,該消息就會被發送到該死信隊列上,再取出訂單信息進行判斷訂單是否已支付,如未支付則講訂單狀態修改為取消狀態,這樣也是可以達到訂單超時取消的需求的。

軟件准備

erlang

請參考Win10下安裝erlang

https://blog.csdn.net/linsongbin1/article/details/80170487

RabbitMQ

https://blog.csdn.net/linsongbin1/article/details/80170567

啟動RabbitMQ,然后添加一個用戶, 並給用戶設置權限。

# 后台啟動
rabbitmq-server -detached
# 添加用戶
rabbitmqctl add_user root 123456
# 設置用戶權限:
rabbitmqctl set_permissions -p "/" root ".*" ".*" ".*"

接下來創建一個springboot工程,並集成RabbitMQ。

<?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 https://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.2.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lagou</groupId>
    <artifactId>rabbitmq-work</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rabbitmq-work</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
            <scope>runtime</scope>
        </dependency>
        <!--mybatis-plus依賴-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

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

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

接下來在application.yml配置文件中假如rabbitmq配置

spring:
  application:
    name: rabbit-work
  # rabbitmq配置
  rabbitmq:
    host: localhost
    virtual-host: /
    username: root
    password: 123456
  # redis配置
  redis:
    timeout: 6000
    host: localhost
    port: 6379
    database: 0
  # 數據源配置
  datasource:
    url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=utf8&useSSL=false
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 123456
    hikari:
      minimum-idle: 3
      maximum-pool-size: 5
      max-lifetime: 30000
      connection-init-sql: SELECT 1
# mybatis-plus配置
mybatis-plus:
  mapper-locations: classpath:com.lagou.*.mapper/*.xml
  type-aliases-package: com.lagou.*.domain

定義RabbitConfig

@Configuration
public class RabbitConfig {

    /**
     * 訂單隊列
     *
     * @return {@link Queue}
     */
    @Bean
    public Queue orderQueue() {
        Map<String, Object> argments = new HashMap<>();
        argments.put("x-message-ttl", 60000);
        argments.put("x-dead-letter-exchange", RabbitConstants.ORDER_DLX_EXCHANGE);
        argments.put("x-dead-letter-routing-key", RabbitConstants.ORDER_DLX_ROUTING_KEY);
        Queue queue = new Queue(RabbitConstants.ORDER_QUEUE, true, false, false, argments);
        return queue;
    }

    /**
     * 訂單交換機
     *
     * @return {@link Exchange}
     */
    @Bean
    public Exchange orderExchange() {
        return new DirectExchange(RabbitConstants.ORDER_EXCHANGE, true, false, null);
    }

    /**
     * 訂單路由鍵
     *
     * @return {@link Binding}
     */
    @Bean
    public Binding orderRouting() {
        return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(RabbitConstants.ORDER_ROUTING_KEY).noargs();
    }

    /**
     * 訂單死信隊列
     *
     * @return {@link Queue}
     */
    @Bean
    public Queue orderDlxQueue() {
        Queue queue = new Queue(RabbitConstants.ORDER_DLX_QUEUE, true, false, false);
        return queue;
    }

    /**
     * 訂單死信交換機
     *
     * @return {@link Exchange}
     */
    @Bean
    public Exchange orderDlxExchange() {
        return new DirectExchange(RabbitConstants.ORDER_DLX_EXCHANGE, true, false, null);
    }

    /**
     * 訂單死信路由鍵
     *
     * @return {@link Binding}
     */
    @Bean
    public Binding orderDlxRouting() {
        return BindingBuilder.bind(orderDlxQueue()).to(orderDlxExchange()).with(RabbitConstants.ORDER_DLX_ROUTING_KEY).noargs();
    }

    /**
     * 庫存隊列
     *
     * @return {@link Queue}
     */
    @Bean
    public Queue stockQueue() {
        return new Queue(RabbitConstants.STOCK_QUEUE, true, false, false, null);
    }

    /**
     * 庫存交換機
     *
     * @return {@link Exchange}
     */
    @Bean
    public Exchange stockExchange() {
        return new DirectExchange(RabbitConstants.STOCK_EXCHANGE, true, false, null);
    }

    /**
     * 庫存路由鍵
     *
     * @return {@link Binding}
     */
    @Bean
    public Binding stockRouting() {
        return BindingBuilder.bind(stockQueue()).to(stockExchange()).with(RabbitConstants.STOCK_ROUTING_KEY).noargs();
    }

}

實現消息發送

@RequestMapping(value = "/submit", produces = "application/json;charset=UTF-8")
public R submit(@RequestBody OrderVo orderVo) throws UnsupportedEncodingException {
	Order order = orderService.createOrder(orderVo);
	System.out.println("OrderController.submit... createOrder");

	// 放入死信隊列
	amqpTemplate.convertAndSend(RabbitConstants.ORDER_EXCHANGE,
			RabbitConstants.ORDER_ROUTING_KEY,
			(order.getId() + ""));
	System.out.println("OrderController.submit... sendMessage to orderExchange");
	return R.ok(order);
}

消息消費者監聽

@Component
public class OrderHandler {

    @Autowired
    private OrderService orderService;

    @RabbitListener(queues = RabbitConstants.ORDER_DLX_QUEUE, ackMode = "MANUAL")
    public void onMessage(Message message, Channel channel) throws IOException {
        System.out.println("消息進入死信隊列...");
        String s = new String(message.getBody());
        Order o = orderService.getById(Long.parseLong(s));
        if (o != null && OrderConstants.TOPAID.getCode().equals(o.getOrderState())) {
            Order order = new Order();
            order.setId(o.getId());
            order.setOrderState(OrderConstants.CANCEL.getCode());
            order.setGmtModified(new Date());
            orderService.updateById(order);
        }
        // 手動ack
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

啟動Spring Boot應用進行測試

點擊其中一個有庫存的商品進行購買,先演示一個成功支付的。

支付成功

接下來再演示支付失敗的,並注意控制台日志打印。

支付超時

控制台日志

通過監聽死信隊列,消息在進入死信隊列之后就可以做一系列業務邏輯處理,比如,消息如果還是未支付狀態,將其修改為取消支付。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM