微服務之五消息驅動
一: 概述
JavaEE提供了Message Driven Bean(消息驅動bean),用於處理企業組件間的消息驅動。
Spring Cloud也引入了相應的驅動,Spring Cloud Stream
市面上存在的消息代理中間件
- ActivitiMQ
- RabbitMQ
- Kafka
類似郵局,有生產者、消息代理、消費者
Stream框架:
- 持久化訂閱的支持
- 消費者組的支持
- Topic分區支持
- 支持RabbitMQ和Kafka兩種消息代理組件
好處:不同考慮中間使用的什麼代理機制,利用Stream實現消息的生產與發送
消息代理中間件模式
使用了Spring Cloud Stream結構後
使用了Stream後,生產者和消費者可以更加專注自己的業務,至於消息是如何投遞、使用的那個消息代理則無需關心
二: RabbitMQ框架
RabbitMQ使用AMQP協議
2.1 消息生產者
消息的生產者/消費者都屬於客戶端,均使用AMQP協議與RabbitMQ服務器進行通信
添加依賴
<!-- AMQP -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.2.0</version>
</dependency>
<!-- SLF4J日誌 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.9</version>
</dependency>
發消息
package com.atm.cloud;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class SendMessage {
public static void main(String[] args) throws Exception {
/*
* 1.生產者會發送消息給RabbitMQ服務器。 2.通過渠道叫消息發送給交換器。 3.交換器會發送給隊列。 4.隊列將消息發送給消費者。
*/
// 建立連接工廠
ConnectionFactory factory = new ConnectionFactory();
// 設置host,其實無需設置,默認爲localhost,用戶名/密碼默認guest,端口默認5671
// factory.setHost("localhost");
// 創建新的連接
Connection connection = factory.newConnection();
// 通過連接創建渠道(向該渠道發送消息)
Channel channel = connection.createChannel();
// 聲明交換器(默認綁定),交換器會將消息發送給隊列,對列再發送給消費者
// 直接聲明隊列,使用默認交換器
String queueName = "MyQueueName";
channel.queueDeclare(queueName, false, false, false, null);
// 創建消息,使用渠道發佈消息,""使用默認交換器,本列子中routingKey就使用queueName
String messageBody = "Hello Wrold!";
channel.basicPublish("", queueName, null, messageBody.getBytes());
// 發送之後,關閉渠道等(先關渠道,再關連接)
channel.close();
connection.close();
}
}
2.2 消費者
package com.atm.cloud;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
public class ReadMessage {
public static void main(String[] args) throws Exception {
// 建立連接工廠
ConnectionFactory factory = new ConnectionFactory();
// 創建新的連接
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String queueName = "MyQueueName";
channel.queueDeclare(queueName, false, false, false, null);
// 通過隊列創建Consumer
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("接收到的消息:" + msg);
}
};
// 渠道綁定consumer
channel.basicConsume(queueName, consumer);
}
}
2.3 交換器、綁定與隊列
在RabbitMQ中,生產者的消息不會直接到隊列中,只會講消息發送給交換器,交換器一邊從生產者接受信息,一邊將信息發送給各個隊列。
消息發送過來,消息自身帶着一個routingKey,交換器根據該key進行隊列的綁定
RabbitMQ提供四種交換器
- direct:根據生產者傳過來的routingKey是否等於bindingkey,來決定將消息發送給哪個隊列
- topic:根據傳過來的routingkey是否匹配一定的表達式,來決定消息發送給哪個或者哪些隊列
- fanout:將消息發送給交換器知道的全部隊列,這種交換器會忽略掉設置的routingkey(廣播機制)
- headers:根據消息的頭消息,來決定將消息發送給哪些隊列
三:Kafka框架
3.1 生產者
依賴
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.9</version>
</dependency>
發消息
package com.atm.cloud;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
/**
* 向Kafka服務器發送消息
*/
public class SendMessage {
public static void main(String[] args) throws Exception {
// 配置信息
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// String的序列化類
// 設置數據key的序列化處理類
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
// 設置數據value的序列化處理類
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
// 創建生產者實例
Producer<String, String> producer = new KafkaProducer<String, String>(
props);
// 創建一條新的記錄,第一個參數爲Topic名稱
// 會向topic發送userName-aitemi鍵值,所有的數據都是通過鍵值保存的
ProducerRecord record = new ProducerRecord<String, String>("my-topic",
"userName", "aitemi");
// 發送記錄
producer.send(record);
producer.close();
}
}
3.2 消費者
package com.atm.cloud;
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
/**
* 消費者,訂閱"my-topic",獲取其中的信息
*/
public class ReadMessage {
public static void main(String[] args) throws Exception {
// 配置信息
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// 必須指定消費者組
props.put("group.id", "test");
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(
props);
// 訂閱 my-topic 的消息,可以訂閱多個topic
consumer.subscribe(Arrays.asList("my-topic"));
// 到服務器中讀取記錄,會一直拉取
while (true) {
// 通過consumer的一個拉取方法
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.println("這是消費者A,key: " + record.key() + ", value: "
+ record.value());
}
}
}
}
3.3 消費者組
在同一個消費者組的,在接受同一個TOPIC時,會體現負載均衡的效果。輪換接受信息。
在不同消費者組的,接受同一個TOPIC時,都會收到信息,廣播的效果
二:開發消息微服務
Spring Cloud Stream簡化了步驟,進行少量的配置可以實現前面兩個框架的功能,不需要調用上面兩種框架的API
2.1 準備工作
三個項目
- spring-service:Eureka 服務端
- spring-consumer:消費者
- spring-producer:生產者
整個集羣如下圖所示:
2.2 application配置
server:
port: 9000
spring:
application:
name: atm-msg-consumer
rabbitmq:
host: localhost
port: 5762
username: guest
password: guest
# 均使用默認的配置,所以可以無需配置,需要修改時,再配置
2.3 生產者
2.3.1 依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
2.3.2 啓動類
添加@EnableBinding註解,指定服務接口
package com.atm.cloud;
import java.util.Scanner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
@SpringBootApplication
@EnableEurekaClient
@EnableBinding(SendMessageInterface.class)//開啓綁定
public class MsgProducerApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String port = scanner.nextLine();
new SpringApplicationBuilder(MsgProducerApp.class).properties(
"server.port=" + port).run(args);
}
}
2.3.3 服務接口
package com.atm.cloud;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.SubscribableChannel;
/**
* 發送消息的服務接口,用來綁定Topic
*/
public interface SendMessageInterface {
/**
* 聲明一個方法用來訂閱渠道,使用output註解,聲明渠道名稱,從這裏輸出一個消息
*/
@Output("myInput")
SubscribableChannel sendMsg();
}
使用@Output註解會創建myInput的消息通道,調用次方法後會向myInput通道投遞消息。
2.3.4 控制層
package com.atm.cloud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 使用SendMessageInterface中的消息渠進行發送消息
*/
@RestController
public class SendMessageController {
@Autowired
private SendMessageInterface sendMessageInterface;
@GetMapping("/send")
public String sendMsg() {
Message msg = MessageBuilder.withPayload("Hello World".getBytes())
.build();
sendMessageInterface.sendMsg().send(msg);
return "Success";
}
}
2.4 消費者
2.4.1 依賴
和生產者的依賴一樣
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
2.4.2 服務接口
接受消息的通道接口
package com.atm.cloud;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
/**
* 用於接收消息
*/
public interface ReadMessageInterface {
// 綁定myInput的渠道
@Input("myInput")
SubscribableChannel readMsg();
}
2.4.3 啓動類和監聽、接受消息
package com.atm.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
@SpringBootApplication
@EnableDiscoveryClient
@EnableBinding(ReadMessageInterface.class)
public class MsgConsumerApp {
public static void main(String[] args) {
SpringApplication.run(MsgConsumerApp.class, args);
}
/**
* 用於監聽接收的消息
*/
@StreamListener("myInput")
public void onListen(byte[] msg) {
System.out.println("接收到的消息:" + new String(msg));
}
}
2.5 更換綁定器
若需要更換爲kafka,則需要在生產者和消費者中同時更改依賴項
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
2.6 消費者組
若需要使用消費者組,則需要更改配置文件
spring:
application:
name: spring-msg-consumer3
##配置消費者組
cloud:
stream:
bindings:
myInput:
group: groupB
2.7 Sink、Source、Processor
- 爲了簡化開發,SpringCloud Stream內置了三個接口,Sink、Source、Processor
- Processor繼承了Sink和Source,實際應用中可以考慮只使用Processor
Sink
public interface Sink {
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
Source
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
SubscribableChannel output();
}
根據這兩個接口可知,實際上幫我們內置了”input”和“output”兩個通道,那麼在大多數情況下,我們就可以不必編寫服務接口,甚至不必使用@input和@output兩個註解,在綁定通道時加入Sink.class
package com.atm.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
@SpringBootApplication
@EnableDiscoveryClient
//@EnableBinding(ReadMessageInterface.class)
@EnableBinding(value={Sink.class})
public class MsgConsumerApp {
public static void main(String[] args) {
SpringApplication.run(MsgConsumerApp.class, args);
}
/**
* 用於監聽接收的消息
*/
//@StreamListener("myOutput)
@StreamListener(Sink.INPUT)
public void onListen(byte[] msg) {
System.out.println("A:接收到的消息:" + new String(msg));
}
}