使用SpringCloud Stream結合rabbitMQ實現訊息消費失敗重發機制

銘言明語發表於2020-06-21

前言:實際專案中經常遇到訊息消費失敗了,要進行訊息的重發。比如支付訊息消費失敗後,要分不同時間段進行N次的訊息重發提醒。

本文模擬場景

  1. 當金額少於100時,訊息消費成功
  2. 當金額大於100,小於200時,會進行3次重發,第一次1秒;第二次2秒;第三次3秒。
  3. 當金額大於200時,訊息消費失敗,會進行5次重發,第一次1秒;第二次2秒;第三次3秒;第四次4秒;第五次5秒。重試五次後,訊息自動進入死信佇列,在死信佇列存活60秒後消失。

程式碼例項

特別注意程式碼與配置檔案中的註釋,各個使用說明都已經詳細寫在配置檔案中

pom包引入

<?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.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cloudstream</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR5</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- ①關鍵配置:引入stream-rabbit 依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <!-- ②關鍵配置:由於stream是基於spring-cloud的,所以這裡要引入 -->
            <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>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

</project>

配置application.yml檔案

注意各個配置的縮排格式,別搞錯了

server:
  port: 8081
spring:
  application:
    name: stream-demo
  #rabbitmq連線配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: 123456
  cloud:
    stream:
      bindings:
        #訊息生產者,與DelayDemoTopic介面中的DELAY_DEMO_PRODUCER變數值一致
        delay-demo-producer:
          #①定義交換機名
          destination: demo-delay-queue
        #訊息消費者,與DelayDemoTopic介面中的DELAY_DEMO_CONSUMER變數值一致
        delay-demo-consumer:
          #定義交換機名,與①一致,就可以使傳送和消費都指向一個佇列
          destination: demo-delay-queue
          #分組,這個配置可以開啟訊息持久化、可以解決在叢集環境下重複消費的問題。
          #比如A、B兩臺伺服器叢集,如果沒有這個配置,則A、B都能收到同樣的訊息,如果有該配置則只有其中一臺會收到訊息
          group: delay-consumer-group
          consumer:
            #最大重試次數,預設為3。不使用預設的,這裡定義為1,由我們程式控制傳送時間和次數
            maxAttempts: 1
      rabbit:
        bindings:
          #訊息生產者,與DelayDemoTopic介面中的DELAY_DEMO_PRODUCER變數值一致
          delay-demo-producer:
            producer:
              #②申明為延遲佇列
              delayedExchange: true
          #訊息消費者,與DelayDemoTopic介面中的DELAY_DEMO_CONSUMER變數值一致
          delay-demo-consumer:
            consumer:
              #申明為延遲佇列,與②的配置的成對出現的
              delayedExchange: true
              #開啟死信佇列
              autoBindDlq: true
              #死信佇列中訊息的存活時間
              dlqTtl: 60000

定義佇列通道

  1. 定義通道
/**
 * 定義延遲訊息通道
 */
public interface DelayDemoTopic {
    /**
     * 生產者,與yml檔案配置對應
     */
    String DELAY_DEMO_PRODUCER = "delay-demo-producer";
    /**
     * 消費者,與yml檔案配置對應
     */
    String DELAY_DEMO_CONSUMER = "delay-demo-consumer";

    /**
     * 定義訊息消費者,在@StreamListener監聽訊息的時候用到
     * @return
     */
    @Input(DELAY_DEMO_CONSUMER)
    SubscribableChannel delayDemoConsumer();

    /**
     * 定義訊息傳送者,在傳送訊息的時候用到
     * @return
     */
    @Output(DELAY_DEMO_PRODUCER)
    MessageChannel delayDemoProducer();
}
  1. 繫結通道
/**
 * 配置訊息的binding
 *
 */
@EnableBinding(value = {DelayDemoTopic.class})
@Component
public class MessageConfig {

}

訊息傳送模擬

/**
 * 傳送訊息
 */
@RestController
public class SendMessageController {
    @Autowired
    DelayDemoTopic delayDemoTopic;

    @GetMapping("send")
    public Boolean sendMessage(BigDecimal money) throws JsonProcessingException {

        Message<BigDecimal> message = MessageBuilder.withPayload(money)
                //設定訊息的延遲時間,首次傳送,不設定延遲時間,直接傳送
                .setHeader(DelayConstant.X_DELAY_HEADER,0)
                //設定訊息已經重試的次數,首次傳送,設定為0
                .setHeader(DelayConstant.X_RETRIES_HEADER,0)
                .build();
        return delayDemoTopic.delayDemoProducer().send(message);
    }
}

訊息監聽處理

@Component
@Slf4j
public class DelayDemoTopicListener {
    @Autowired
    DelayDemoTopic delayDemoTopic;

    /**
     * 監聽延遲訊息通道中的訊息
     * @param message
     */
    @StreamListener(value = DelayDemoTopic.DELAY_DEMO_CONSUMER)
    public void listener(Message<BigDecimal> message) {
        //獲取重試次數
        int retries = (int)message.getHeaders().get(DelayConstant.X_RETRIES_HEADER);
        //獲取訊息內容
        BigDecimal money = message.getPayload();
        try {
            String now = DateUtils.formatDate(new Date(),"yyyy-MM-dd HH:mm:ss");
            //模擬:如果金額大於200,則訊息無法消費成功;金額如果大於100,則重試3次;如果金額小於100,直接消費成功
            if (money.compareTo(new BigDecimal(200)) == 1){
                throw new RuntimeException(now+":金額超出200,無法交易。");
            }else if (money.compareTo(new BigDecimal(100)) == 1 && retries <= 3) {
                if (retries == 0) {
                    throw new RuntimeException(now+":金額超出100,消費失敗,將進入重試。");
                }else {
                    throw new RuntimeException(now+":金額超出100,當前第" + retries + "次重試。");
                }
            }else {
                log.info("訊息消費成功!");
            }
        }catch (Exception e) {
            log.error(e.getMessage());
            if (retries < DelayConstant.X_RETRIES_TOTAL){
                //將訊息重新塞入佇列
                MessageBuilder<BigDecimal> messageBuilder = MessageBuilder.fromMessage(message)
                        //設定訊息的延遲時間
                        .setHeader(DelayConstant.X_DELAY_HEADER,DelayConstant.ruleMap.get(retries + 1))
                        //設定訊息已經重試的次數
                        .setHeader(DelayConstant.X_RETRIES_HEADER,retries + 1);
                Message<BigDecimal> reMessage = messageBuilder.build();
                //將訊息重新傳送到延遲佇列中
                delayDemoTopic.delayDemoProducer().send(reMessage);
            }else {
                //超過重試次數,做相關處理(比如儲存資料庫等操作),如果丟擲異常,則會自動進入死信佇列
                throw new RuntimeException("超過最大重試次數:" + DelayConstant.X_RETRIES_TOTAL);
            }
        }
    }
}

規則定義

目前寫在一個常量類裡,實際專案中,通常會配置在配置檔案中

public class DelayConstant {
    /**
     * 定義當前重試次數
     */
    public static final String X_RETRIES_HEADER = "x-retries";
    /**
     * 定義延遲訊息,固定值,該配置放到訊息的header中,會開啟延遲佇列
     */
    public static final String X_DELAY_HEADER = "x-delay";

    /**
     * 定義最多重試次數
     */
    public static final Integer X_RETRIES_TOTAL = 5;

    /**
     * 定義重試規則,毫秒為單位
     */
    public static final Map<Integer,Integer> ruleMap = new HashMap(){{
        put(1,1000);
        put(2,2000);
        put(3,3000);
        put(4,4000);
        put(5,5000);
    }};
}

測試

經過以上配置和實現就可完成模擬的重發場景。

  • 瀏覽器中輸入http://127.0.0.1:8081/send?money=10,可以看到控制檯中輸出:
訊息消費成功!
  • 瀏覽器中輸入http://127.0.0.1:8081/send?money=110,可以看到控制檯中輸出:
2020-06-20 10:59:42:金額超出100,消費失敗,將進入重試。
2020-06-20 10:59:43:金額超出100,當前第1次重試。
2020-06-20 10:59:45:金額超出100,當前第2次重試。
2020-06-20 10:59:48:金額超出100,當前第3次重試。
訊息消費成功!

  • 瀏覽器中輸入http://127.0.0.1:8081/send?money=110,可以看到控制檯中輸出:
    在這裡插入圖片描述

注意事項

由於本文用到了延遲佇列,需要在rabbitMQ中安裝延遲外掛,具體安裝方式,可以檢視:延遲佇列安裝參考

原始碼獲取

以上示例都可以通過我的GitHub獲取完整的程式碼.

相關文章