微服务之五消息驱动

一: 概述

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章