微服務之五消息驅動

一: 概述

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));
    }
}
發佈了21 篇原創文章 · 獲贊 1 · 訪問量 344
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章