Spring Cloud Stream微服務訊息框架

freshchen發表於2020-05-30

簡介

隨著近些年微服務在國內的盛行,訊息驅動被提到的越來越多。主要原因是系統被拆分成多個模組後,一個業務往往需要在多個服務間相互呼叫,不管是採用HTTP還是RPC都是同步的,不可避免快等慢的情況發生,系統效能上很容易遇到瓶頸。在這樣的背景下,將業務中實時性要求不是特別高且非主幹的部分放到訊息佇列中是很好的選擇,達到了非同步解耦的效果。

目前訊息佇列有很多優秀的中介軟體,目前使用較多的主要有 RabbitMQ,Kafka,RocketMQ 等,這些中介軟體各有優勢,有的對 AMQP(應用層標準高階訊息佇列協議)支援完善,有的提供了更高的可靠性,有的對大資料支援良好,同時各種訊息中介軟體概念不統一,使得選擇和使用一款合適的訊息中介軟體成為難題。Spring跳出來給出瞭解決方案:Spring Cloud Stream,使用它可以很方便高效的操作訊息中介軟體,程式設計師只要關心業務程式碼即可,目前官方支援 RabbitMQ,Kafka兩大主流MQ,RocketMQ 則自己提供了相應支援。

首先看一下Spring Cloud Stream做了什麼,如下圖所示,框架目前官方把訊息中介軟體抽象成了 Binder,業務程式碼通過進出管道連線 Binder,各訊息中介軟體的差異性統一交給了框架處理,程式設計師只需要瞭解框架的抽象出來的一些統一概念即可

  • Binder(繫結器):RabbitMQ,Kafka等中介軟體服務的封裝
  • Channel(管道):也就是圖中的 inputs 和 outputs 所指區域,是應用程式和 Binder 的橋樑
  • Gourp(消費組):由於微服務會部署多例項,為了保證只被服務的一個例項消費,可以通過配置,把例項都綁到同一個消費組
  • Partitioning (訊息分割槽):如果某一類訊息只想指定給服務的固定例項消費,可以使用分割槽實現

Spring Cloud Stream將業務程式碼和訊息中介軟體解耦,帶來的好處可以從下圖很直觀的感受到,很簡潔的程式碼,我們便能從RabbitMQ中接受訊息然後經過業務處理再向Kafka傳送一條訊息,只需要更改相關配置就能快速改變系統行為。

細心的讀者可能會好奇,上圖的程式碼只是注入了一個簡單的 Function 而已,實際上,Spring Cloud Stream3.0後整合了Spring Cloud Function框架 ,提倡函式式的風格,棄用先前版本基於註解的開發方式。Spring Cloud Function是 Serverless 和 Faas 的產物,強調面向函式程式設計,一份程式碼各雲平臺執行,和Spring Cloud Stream一樣也是解決了基礎設施的差異性問題,通過強大的自動裝配機制,可以根據配置自動暴露 HTTP 服務或者訊息服務,並且同時支援命令式和響應式程式設計模式,可以說是很強大了。下面通過一個簡單的例子來理解下上圖的程式碼和框架的使用把。

簡單案例

模擬一個簡單的下單,收到訂單之後處理完,返回成功,然後傳送訊息給庫存模組,庫存模組再傳送訊息給報表模組

專案地址

springcloud-stream

專案結構

專案依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

表單

@Data
public class OrderForm {
    private String productName;
}

訊息管道註冊

@Configuration
@Slf4j
public class MessageQueueConfig {

    @Bean
    public Function<OrderForm, OrderForm> inventory() {
        return orderForm -> {
            log.info("Inventory Received Message: " + orderForm);
            return orderForm;
        };
    }

    @Bean
    public Consumer<OrderForm> report() {
        return orderForm -> {
            log.info("Report Received Message: " + orderForm);
        };
    }
}

Controller

@Slf4j
@RestController
public class OrderController {

    @Autowired
    private BeanFactoryChannelResolver resolver;

    @PostMapping("order")
    public String order(@RequestBody OrderForm orderForm) {
        log.info("Received Request " + orderForm);
        resolver.resolveDestination("inventory-in-0").send(new GenericMessage<>(orderForm));
        return "success";
    }
}

配置

框架會按照中介軟體預設埠去連線,這裡自定義了一個名為myLocalRabbit的型別是RabbitMQ的Binder配置,bindings下面 inventory-in-0 是通道名,接受inventory主題(對應RabbitMQ的ExChange)的訊息,然後處理完通過 inventory-out-0 通道傳送訊息到 report 主題, report-in-0通道負責接受report主題的訊息。

注:通道名=註冊的 function 方法名 + in或者out + 引數位置(詳見註釋)

spring:
  cloud:
    stream:
#     配置訊息中介軟體資訊
      binders:
        myLocalRabbit:
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: localhost
                port: 31003
                username: guest
                password: guest
                virtual-host: /
#     重點,如何繫結通道,這裡有個約定,開頭是函式名,in表示消費訊息,out表示生產訊息,最後的數字是函式接受的引數的位置,destination後面為訂閱的主題
#     比如Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<String>> gather()
#     gather函式接受的第一個String引數對應 gather-in-0,第二個Integer引數對應 gather-in-1,輸出對應 gather-out-0
      bindings:
        inventory-in-0:
          destination: inventory
        inventory-out-0:
          destination: report
        report-in-0:
          destination: report
#     註冊宣告的三個函式
      function:
        definition: inventory;report

測試

POST http://localhost:8080/order
Content-Type: application/json

{
  "productName": "999"
}

結果

POST http://localhost:8080/order

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 7
Date: Sat, 30 May 2020 15:27:56 GMT
Keep-Alive: timeout=60
Connection: keep-alive

success

Response code: 200; Time: 56ms; Content length: 7 bytes

後臺日誌

可以看到訊息成功傳送到了庫存和報表服務

2020-05-30 23:27:56.956  INFO 8760 --- [nio-8080-exec-1] c.e.springcloudstream.OrderController    : Received Request OrderForm(productName=999)
2020-05-30 23:27:56.956  INFO 8760 --- [nio-8080-exec-1] o.s.i.h.s.MessagingMethodInvokerHelper   : Overriding default instance of MessageHandlerMethodFactory with provided one.
2020-05-30 23:27:56.957  INFO 8760 --- [nio-8080-exec-1] c.e.s.MessageQueueConfig                 : Inventory Received Message: OrderForm(productName=999)
2020-05-30 23:27:56.958  INFO 8760 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:31003]
2020-05-30 23:27:56.964  INFO 8760 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory.publisher#6131841e:0/SimpleConnection@192fe472 [delegate=amqp://guest@127.0.0.1:31003/, localPort= 2672]
2020-05-30 23:27:56.965  INFO 8760 --- [nio-8080-exec-1] o.s.amqp.rabbit.core.RabbitAdmin         : Auto-declaring a non-durable, auto-delete, or exclusive Queue (inventory.anonymous.wtaFwHlNRkql5IUh2JCNAA) durable:false, auto-delete:true, exclusive:true. It will be redeclared if the broker stops and is restarted while the connection factory is alive, but all messages will be lost.
2020-05-30 23:27:56.965  INFO 8760 --- [nio-8080-exec-1] o.s.amqp.rabbit.core.RabbitAdmin         : Auto-declaring a non-durable, auto-delete, or exclusive Queue (report.anonymous.SJgpJKiJQf2tudszgf623w) durable:false, auto-delete:true, exclusive:true. It will be redeclared if the broker stops and is restarted while the connection factory is alive, but all messages will be lost.
2020-05-30 23:27:56.979  INFO 8760 --- [f2tudszgf623w-1] o.s.i.h.s.MessagingMethodInvokerHelper   : Overriding default instance of MessageHandlerMethodFactory with provided one.
2020-05-30 23:27:56.980  INFO 8760 --- [f2tudszgf623w-1] c.e.s.MessageQueueConfig                 : Report Received Message: OrderForm(productName=999)

相關文章