事情的起因源於在使用微信公衆號服務的時候,作爲一個第三方的服務商,騰訊會將各種業務消息推送到第三方開發者的服務器上,而之前的方案是消息直接進到服務上,當使用到一些業務,比如發券等操作時,騰訊服務器會向開發者發送大量的消息,由於消息服務的處理能力有限,尤其是高峯的時候,消息請求會直接壓到服務上,導致服務線程繁忙,這時候會報大量服務超時,觸發微信的服務報警,服務不可用,或者服務超時,這時公衆號內的消息服務將無法繼續爲用戶提供服務。鑑於此問題,我們重新梳理並構建了基於Spring Cloud Stream的消息驅動的微服務
我們採用 Spring Cloud Finchley.RELEASE ,Spring Boot 2.0.1.RELEASE 版本來開發,系統的初步設計思路,是利用
消息隊列rabbitmq來解耦服務,來減緩消息直接到服務上的壓力,我們沒有直接對接mq來使用,而是採用了Spring Cloud Stream, 簡單的來說,Spring Cloud Stream是構建消息驅動的微服務應用程序的框架,將消息整合的處理操作進行了進一步的抽象操作, 實現了更加簡化的消息處理, 可以使用不同的代理中間件,它抽象了事件驅動的一些概念,對於消息中間件的進一步封裝,可以做到代碼層面對中間件的無感知,甚至於動態的切換中間件,切換topic。使得微服務開發的高度解耦,服務可以關注更多自己的業務流程。
Spring Cloud Stream的一些基本概念可以自行搜索,這裏不做過多描述,下面只是講述一下具體方案和配置方法及遇到的問題。
基本結構是一個非常簡單的消息訂閱發佈模式
message-center 作爲接受微信消息處理中心,爲消息生產者,
message-for-all 作爲消息隊列消息處理服務,爲消息消費者,
message-center 從微信的服務器接受到消息後,採用異步多線程的方式,處理部分業務邏輯,比如多客服,比如第三方的全網發佈檢測等,需要在5秒內返回給微信服務器消息的一些及時性消息,同時根據消息類型講消息分類,併發送給消息隊列中間件,消息生產者message-center通過SpringCloudStream來作爲和消息隊列中間件的粘合劑,將消息傳遞給消息隊列中間件,這裏可以隨意切換消息中間件而不用考慮代碼的變更,我們這裏默認採用的rabbitmq作爲消息隊列中間件服務,
具體配置方法,新建一個接口,這個接口中可以定義多個管道用來發送消息,可以實現向不同的exchange發送消息
public interface SendOutputChannel {
// 這裏可以定義不同的通道
String MSG_SENDER = "msgSender"; // 通道名
@Output(SendOutputChannel.MSG_SENDER)
MessageChannel msgSender();
}
啓動的類中要加入@EnableBinding
@SpringBootApplication
@EnableBinding(SendOutputChannel.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml文件中配置
#---- mq的基本配置信息
spring:
rabbitmq:
host:
port:
username:
password:
virtual-host:
cloud:
stream:
bindings:
msgSender: #生產者綁定,這個是消息通道的名稱
destination: message-exchange # 這裏對應的是rabbitmq中的exchange
content-type: application/json #定義消息類型
發送消息的類中要注入發送消息的接口,當接到微信發送的消息後,經過業務邏輯處理後,在需要向mq發消息的地方,調用發送消息的方法,通過Spring Cloud Stream來實現消息發送。
@Autowired
private SendOutputChannel sendOutputChannel;
public void sendMessage(Message<?> message) {
if (!sendOutputChannel.msgSender().send(message, TimeUnit.SECONDS.toMillis(4))) {
log.error("生產者消息發送失敗:" + message.toString());
}
}
這就完成了消息發送者的基本開發
這樣服務啓動以後,rabbitmq的exchange中將會出現一個 message-exchange的 交換機
消息接受者 message-for-all
同樣需要定義一個消息接收的管道接口,這個接口中可以定義多個管道用來接受消息,可以接受對應不同的exchange接受到的消息
public interface ReceiveInputChannel {
// 這裏可以定義不同的通道
String MSG_RECEIVER = "msgReceiver"; // 通道名
@Input(ReceiveInputChannel.MSG_RECEIVER)
SubscribableChannel msgReceiver();
}
application.yml中配置
#---- mq的基本配置信息
spring:
rabbitmq:
host:
port:
username:
password:
virtual-host:
cloud:
stream:
bindings:
msgReceiver: #消費者綁定 這個是接受消息通道的名稱
group: for-all #持久化, 也就是指定隊列名稱,等同於rabbitmq中的 queue, 同一個服務不同的實例,使用相同的隊列名稱,處理消息
destination: message-exchange #和生產者的消息交換機要相同
content-type: application/json
consumer:
max-attempts: 3 # The number of attempts to process the message (including the first) in the event of processing failures,Default: 3
concurrency: 1 # The concurrency setting of the consumer. Default: 1.
rabbit:
bindings:
msgReceiver:
consumer:
max-concurrency: 4 # maxumum concurrency of this consumer (threads)
prefetch: 5 # number of prefetched messages pre consumer thread
requeue-rejected: false # true to requeue rejected messages, false to discard (or route to DLQ)
republish-to-dlq: true # republish failures to the DLQ with diagnostic headers
其他一些配置都是消息每次有幾個線程處理,每個線程處理多少數量的消息,失敗後重新嘗試處理幾次等,一些配置方案,
啓動類裏要使用@EnableBinding綁定消息接收管道
@SpringBootApplication
@EnableBinding(ReceiveInputChannel.class)
public class Application {
@StreamListener(ReceiveInputChannel.MSG_RECEIVER)
public void handle(Message<String> message) throws Exception {
// 在這裏再去整合消息處理的業務邏輯
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
這樣服務啓動以後,將會出現message-exchange.for-all的一個消息隊列,根據消費者服務啓動數量的不同,也將會出現對應的消費者
至此整個消息處理的基本結構就描述完成了,當然實際的開發過程中,還要考慮消息的異步處理,多線程去處理等,這裏就不詳盡描述了,需要根據自己的業務需要來實現相應的開發,
有幾點注意的情況:
@StreamListener(ReceiveInputChannel.MSG_RECEIVER) 這個方法只能放到Application 服務啓動的類中,放到別的地方會報錯:Disp org.springframework.integration.MessageDispatchingException: Dispatcher has no subscribers,可能和類的加載順序有關係。這樣在啓動類中接受消息,然後可以再通過業務拆分,將消息轉到其他的類中實現各自業務邏輯開發。
<!-- 添加Spring Cloud Stream與RabbitMQ消息中間件的依賴。 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- 添加Spring Cloud Stream與Kafaka消息中間件的依賴。 -->
<!-- <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency> -->
根據不同的消息中間件,選擇不同的依賴。
當接收消息過多的時候,可以增加消息生產者實例來加大消息的接受能力,當消費者處理大量阻塞消息時,處理能力下降,可以通過增加負載的消費者服務實例數量來加大消費能力,這個需要通過實際情況找到平衡點,消息隊列作爲緩存,降低了由於消息直接壓到服務器上而導致的服務崩潰問題的風險。