【SpringBoot 2学习笔记】《十三》SpringBoot2消息之旅,带你体验RabbitMQ和Kafka消息发送

SpringBoot消息之旅

消息队列一般的正常业务开发中基本上不怎么涉猎,都是系统整合或特殊业务场景下才有用武之地。所以大部分程序员在工作中基本没有怎么接触。本章主要内容,就是带领大家入门体验一下消息队列的使用过程,扩展一下知识体系,方便今后有需要时根据实际的业务场景进行定制开发。为了方便,本文中使用的RabbitMQ直接使用了阿里的云服务,这个是按照发送消息的次数计费,比较便宜。Kafka服务的话,阿里云的服务比较贵,就没有再购买服务,自己在原来买的ECS服务器上安装了一下,方便进行程序的开发调试。

消息队列(Message Queue)简称为MQ,是应用程序对应用程序的通信方法,也是分布式系统中必不可少的组件,主要用来解决应用解耦、异步消息、流量削峰等问题,实现高性能、高可用、可伸缩和最终一致性的架构。如今比较常用的开源消息队列的组件包括:RabbitMQ和Kafka等。

消息队列基本作用:

  • 程序解耦,生产者和消费者独立,各自异步执行
  • 消息数据进行持久化存储,直到被全部消费,规避了数据丢失风险
  • 流量削峰,使用消息队列承接访问压力,尽量避免程序雪崩
  • 降低进程间的耦合度,系统部分组件崩溃时,不会影响到整个系统
  • 保证消息顺序执行,解决特定场景业务需求

13.1 RabbitMQ消息队列

AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,用于在分布式系统中存储转发消息。

13.1.1 RabbitMQ基础概念

RabbitMQ本身是一个消息代理,他的主要工作就是接收和转发消息。

  • Producer:消息的投递方即生产者。生产者创建消息,然后发布到RabbitMQ中。消息一般可以包含2个部分:(消息体和标签label)。在实际应用中,消息体一般是一个带有业务逻辑结构的数据,可以使数据对象,也可以是JSON字符串。当然可以进一步对这个消息体进行序列化操作。消息的标签来表述这条消息,比如一个交换机的名称和一个路由键。生产者把消息交由RabbitMQ,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者(Consumer)。

  • Consumer :消息接收方即消费者。消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。

  • Queue :队列,是RabbitMQ的内部对象,用于存储消息。 RabbitMQ消息都只能存储在队列中,RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin即轮询)多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。

  • Exchange:交换机。生产者将消息发送到Exchange(交换机,通常也可以用大写的"X"来表示),由交换机将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。这里可以将RabbitMQ中的交换机看作一个简单的实体。RabbitMQ常用的交换机类型有fanout、direct、topic、headers这四种。

  • RoutingKey:路由键。生产者将消息发给交换机的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个RoutingKey需要与交换机类型和绑定键( BindingKey )联合使用才能最终生效。在交换机类型和绑定键( BindingKey )固定的情况下,生产者可以在发送消息给交换机时,通过指定RoutingKey来决定消息流向哪里。

  • Binding:绑定。RabbitMQ中通过绑定将交换机与队列关联起来,在绑定的时会指定绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了。生产者将消息发送给交换机时,需要一个RoutingKey,当BindingKey 和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey ,而是将消息路由到所有绑定到该交换器的队列中。

13.1.2 SpringBoot2中使用RabbitMQ

本小节主要来介绍使用阿里云的AMQP服务开进行演示Demo的开发。注册阿里云账户并开通AMQP服务(这里不做过多介绍),本人选用的是默认的按量付费。

  • 创建阿里云AMQP服务

  • 创建SpringBoot工程

  • 编写配置文件

  • 编写生产者和消费者

1、AMQP开启准备

开通阿里云的AMQP服务后,需要进行初始化的设定,才能进行相应的开发,主要有如下几个步骤:

  • 创建Vhost
  • 创建Exchange(默认使用Direct)
  • 创建Queue(默认设定)
  • 创建阿里云AccessKey

详细内容参考官方网址

2、Exchange说明

Producer将消息发送到Exchange ,由 Exchange 将消息路由到一个或多个Queue中(或者丢弃)。Exchange 根据Routing Key 和Binding Key将消息路由到Queue。不同类型的Exchange的路由规则不同。消息队列AMQP版目前支持三种类型的Exchange。

  • Fanout:该类型路由规则非常简单,会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,相当于广播功能。阿里云控制台绑定时,必须Binding Key,在该模式下随意输入。

  • Direct:该类型路由规则会将消息路由到Binding Key与Routing Key完全匹配的 Queue 中。

  • Topic:该类型与 Direct 类型相似,只是规则没有那么严格,可以模糊匹配和多条件匹配,即该类型Exchange使用 Routing Key 模式匹配和字符串比较的方式将消息路由至绑定的 Queue。

    示例:Routing Key为 use.name 的消息会转发给绑定匹配模式为 *.nameuse.name*.* 的Queue。

13.1.2.1 创建SpringBoot2的AMQP工程

参考《三》基于Maven多模块项目搭建 进行工程项目的搭建。创建一个子工程:gavinbj-mqprocess

POM文件引入相关依赖

	<dependency>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-amqpartifactId>
	dependency>
	<dependency>
		<groupId>com.alibaba.mq-amqpgroupId>
		<artifactId>mq-amqp-clientartifactId>
		<version>1.0.5version>
	dependency>

application.properties中阿里云AMQP的相关配置如下:

 #接入点根据控制台获取接入点
 spring.rabbitmq.host=183692890418XX.mq-amqp.cn-shenzhen-429403-a.aliyuncs.com
 #端口 默认5672
 spring.rabbitmq.port=5672
 #accessKey  ram控制台获取用户子账户ak
 spring.rabbitmq.username=LTAI4FtHr6bE5UqptHnJX2XX
 #secretKey ram控制台获取用户子账户ak
 spring.rabbitmq.password=Pv9g0Si17hl5iu4AUauPoYo3uPZUXX
 #virtual-host 你要使用的virtaulhost
 spring.rabbitmq.virtual-host=gavinbj

具体内容取得可以参考阿里云的官方说明文档。

本章使用的Demo的消息队列发送消息,使用数据实体进项消息的承载,具体人员信息结构可以参考《八》SpringBoot2数据库访问之整合MyBatis 中的UserInfo。

package com.gavinbj.mqprocess.entity;

import java.io.Serializable;
import java.util.Date;

/**
 * user_info
 * @author 
 */
public class UserInfo implements Serializable {
    private String userId;

    /**
     * 用户姓名
     */
    private String userName;

    /**
     * 个人介绍
     */
    private String introduce;

    /**
     * 移动电话
     */
    private String mobilephone;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 生日
     */
    private Date birthday;

    /**
     * 性别
     */
    private String gender;

    private static final long serialVersionUID = 1L;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getIntroduce() {
        return introduce;
    }

    public void setIntroduce(String introduce) {
        this.introduce = introduce;
    }

    public String getMobilephone() {
        return mobilephone;
    }

    public void setMobilephone(String mobilephone) {
        this.mobilephone = mobilephone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        }
        if (that == null) {
            return false;
        }
        if (getClass() != that.getClass()) {
            return false;
        }
        UserInfo other = (UserInfo) that;
        return (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
            && (this.getUserName() == null ? other.getUserName() == null : this.getUserName().equals(other.getUserName()))
            && (this.getIntroduce() == null ? other.getIntroduce() == null : this.getIntroduce().equals(other.getIntroduce()))
            && (this.getMobilephone() == null ? other.getMobilephone() == null : this.getMobilephone().equals(other.getMobilephone()))
            && (this.getEmail() == null ? other.getEmail() == null : this.getEmail().equals(other.getEmail()))
            && (this.getBirthday() == null ? other.getBirthday() == null : this.getBirthday().equals(other.getBirthday()))
            && (this.getGender() == null ? other.getGender() == null : this.getGender().equals(other.getGender()));
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
        result = prime * result + ((getUserName() == null) ? 0 : getUserName().hashCode());
        result = prime * result + ((getIntroduce() == null) ? 0 : getIntroduce().hashCode());
        result = prime * result + ((getMobilephone() == null) ? 0 : getMobilephone().hashCode());
        result = prime * result + ((getEmail() == null) ? 0 : getEmail().hashCode());
        result = prime * result + ((getBirthday() == null) ? 0 : getBirthday().hashCode());
        result = prime * result + ((getGender() == null) ? 0 : getGender().hashCode());
        return result;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", userId=").append(userId);
        sb.append(", userName=").append(userName);
        sb.append(", introduce=").append(introduce);
        sb.append(", mobilephone=").append(mobilephone);
        sb.append(", email=").append(email);
        sb.append(", birthday=").append(birthday);
        sb.append(", gender=").append(gender);
        sb.append(", serialVersionUID=").append(serialVersionUID);
        sb.append("]");
        return sb.toString();
    }
}

接下来我们将介绍Direct模式、Topic模式、Fanout Exchange模式三种来进行消息的发送和接收

13.1.2.2 Direct方式消息发送

Direct模式:简单消息发送模式,发送者将消息发送到指定的消息队列,然后由消息队列转发到消费者。该类型路由规则会将消息路由到 Binding Key与Routing Key完全匹配的 Queue 中。

阿里云AMQP绑定设定实例如下:

源Exchange 绑定目标类型 目标Queue Binding key
gavinbj.direct Queue direct.queue direct

【SpringBoot 2学习笔记】《十三》SpringBoot2消息之旅,带你体验RabbitMQ和Kafka消息发送_第1张图片

  • 图例说明:生产者P发送消息时Routing key = bloking时,这时候将消息传送到Exchange,Exchange获取到生产者发送过来的消息后,会根据自身的规则进行与匹配响应的Queue,这时候发现Queue1和Queue2都符合,就会将消息传送给这两个队列,如果我们以Routing key = create和routing key = confirm发送消息时,这时候消息只会被推送到Queue2队列中,其他的Routing key 的消息会被丢弃。

1、创建AMQP的配置类

使用注解@Configuration 用来标记配置类。

AmqpConfig.java

package com.gavinbj.mqprocess.config;

import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AmqpConfig {
	
    // 阿里云控制台
    private static final String  INSTANCE_ID="1836928904186087";
    
    @Autowired
    private RabbitProperties rabbitProperties;

    @Bean
    public ConnectionFactory getConnectionFactory() {
        com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory =
                new com.rabbitmq.client.ConnectionFactory();
        rabbitConnectionFactory.setHost(rabbitProperties.getHost());
        rabbitConnectionFactory.setPort(rabbitProperties.getPort());
        rabbitConnectionFactory.setVirtualHost(rabbitProperties.getVirtualHost());

        AliAmqpCredentialsProvider credentialsProvider = new AliAmqpCredentialsProvider(
                rabbitProperties.getUsername(), rabbitProperties.getPassword(), INSTANCE_ID);
        rabbitConnectionFactory.setCredentialsProvider(credentialsProvider);
        rabbitConnectionFactory.setAutomaticRecoveryEnabled(true);
        rabbitConnectionFactory.setNetworkRecoveryInterval(5000);
        ConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitConnectionFactory);
        ((CachingConnectionFactory)connectionFactory).setPublisherReturns(rabbitProperties.isPublisherReturns());
        return connectionFactory;
    }

}

2、实现阿里云AMQP的认证代理

AliAmqpCredentialsProvider.java

package com.gavinbj.mqprocess.config;

import org.springframework.util.StringUtils;

import com.alibaba.mq.amqp.utils.UserUtils;
import com.rabbitmq.client.impl.CredentialsProvider;

public class AliAmqpCredentialsProvider implements CredentialsProvider {
	/**
	 * Access Key ID.
	 */
	private final String accessKeyId;
	/**
	 * Access Key Secret.
	 */
	private final String accessKeySecret;
	/**
	 * security temp token. (optional)
	 */
	private final String securityToken;
	/**
	 * instanceId(实例Id,可从AMQP控制台首页获取)
	 */
	private final String instanceId;

	public AliAmqpCredentialsProvider(final String accessKeyId, final String accessKeySecret, final String instanceId) {
		this(accessKeyId, accessKeySecret, null, instanceId);
	}

	public AliAmqpCredentialsProvider(final String accessKeyId, final String accessKeySecret, final String securityToken,
			final String instanceId) {
		this.accessKeyId = accessKeyId;
		this.accessKeySecret = accessKeySecret;
		this.securityToken = securityToken;
		this.instanceId = instanceId;
	}

	@Override
	public String getUsername() {
		if (!StringUtils.isEmpty(securityToken)) {
			return UserUtils.getUserName(accessKeyId, instanceId, securityToken);
		} else {
			return UserUtils.getUserName(accessKeyId, instanceId);
		}
	}

	@Override
	public String getPassword() {
		try {
			return UserUtils.getPassord(accessKeySecret);
		} catch (Exception e) {
		}
		return null;
	}
}

3、创建一个消息发送者

一般使用AmqpTemplate来进行消息发送的操作,在该类中我们创建一个发送消息的方法,并调用convertAndSend方法进行消息发送,代码如下:

AmqpSender.java

package com.gavinbj.mqprocess.sender;

import java.util.Date;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Message发送者
 * 
 * @author gavinbj
 *
 */
@Component
public class AmqpSender {
	
	@Autowired
	private AmqpTemplate amqpTemplate;

	/**
	 * Direct模式发送消息
	 * 
	 */
	public void sendAmqpDirect() {
		
		String exchange = "gavinbj.direct";
		String routingKey = "direct";
		
		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("gavinbj");
		userInfo.setUserName("黑白猿");
		userInfo.setMobilephone("13940989820");
		userInfo.setEmail("[email protected]");
		userInfo.setIntroduce("欢迎光临");
		userInfo.setGender("男");
		userInfo.setBirthday(new Date());
		
		amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
	}
	
}

4、消息接收者

使用@RabbitListener来设定监听队列的方法,方法的参数是接收到实体对象,代码如下:

AmqpReceiver.java

package com.gavinbj.mqprocess.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Message消费者
 * 
 * @author gavinbj
 *
 */
@Component
public class AmqpReceiver {
	
	/**
	 * Direct队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "direct.queue")
    public void processDirect(UserInfo userInfo) {
        System.out.println("Receiver Direct: " + userInfo.toString());
    }
    
}

5、创建Controller进行测试

调用生产者进行消息的发送,消息的接收者收到消息后打印消息内容。

AmqpController.java

package com.gavinbj.mqprocess.controller;

import javax.annotation.Resource;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.AmqpSender;

@RestController
@RequestMapping("/api")
public class AmqpController {

    @Resource
    private AmqpSender sender;

    @RequestMapping(value="/messages/direct")
    public BaseResult<String> sendMessageDirect(){
    	
        sender.sendAmqpDirect();
		System.out.println("简单消息发送成功");
        return ResponseUtils.makeOKRsp("OK");
    }

}

6、启动工程进行测试:

访问地址:http://localhost:9004/gavin/api/messages/direct

返回结果:

{
	"status": 0,
	"code": 1003,
	"msg": "处理成功!",
	"data": "OK"
}

控制台输出结果:

2020-03-11 14:59:24.306  INFO 17292 --- [           main] o.a.coyote.http11.Http11NioProtocol      : Starting ProtocolHandler ["http-nio-9004"]
2020-03-11 14:59:24.329  INFO 17292 --- [           main] com.gavinbj.mqprocess.AmqpApplication    : Started AmqpApplication in 2.641 seconds (JVM running for 3.213)
2020-03-11 14:59:25.805  INFO 17292 --- [nio-9004-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/gavin]  : Initializing Spring DispatcherServlet 'dispatcherServlet'
简单消息发送成功
Receiver Direct: UserInfo [Hash = -202819125, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 14:59:25 CST 2020, gender=男, serialVersionUID=1]

从上可以看出,使用Direct方式消息发送成功,消费者也成功接收到了信息。另外,通过阿里云的控制台也可以看到消息被消费的记录。

13.1.2.3 Topic转发模式消息发送

Topic转发模式主要是通过设置主题的方式进行消息的发送和接收。该类型与 Direct 类型相似,只是规则没有那么严格,可以模糊匹配和多条件匹配,即该类型Exchange使用Routing Key模式匹配和字符串比较的方式将消息路由至绑定的Queue。

阿里云AMQP绑定设定实例如下:

源Exchange 绑定目标类型 目标Queue Binding key
gavinbj.topic Queue topic.queue2 topic.*
gavinbj.topic Queue topic.queue1 topic.zhangsan

【SpringBoot 2学习笔记】《十三》SpringBoot2消息之旅,带你体验RabbitMQ和Kafka消息发送_第2张图片

说明:当生产者发送消息Routing Key=F.C.E的时候,这时候只满足Queue1,所以会被路由到Queue中,如果Routing Key=A.C.E这时候会被同是路由到Queue1和Queue2中,如果Routing Key=A.F.B时,这里只会发送一条消息到Queue2中。

1、创建消息发送者

直接利用上一节中的发送者,添加一个发送Topic消息的方法,并调用convertAndSend方法进行消息发送,代码如下:

AmqpSender.java

package com.gavinbj.mqprocess.sender;

import java.util.Date;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Message发送者
 * 
 * @author gavinbj
 *
 */
@Component
public class AmqpSender {
	
	@Autowired
	private AmqpTemplate amqpTemplate;
	
	/**
	 * Topic模式发送消息
	 * 
	 */
	public void sendAmqpTopic() {
		
		String exchange = "gavinbj.topic";
		String routingKey = "topic.zhangsan";
		
		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("gavinbj");
		userInfo.setUserName("黑白猿");
		userInfo.setMobilephone("13940989820");
		userInfo.setEmail("[email protected]");
		userInfo.setIntroduce("欢迎光临");
		userInfo.setGender("男");
		userInfo.setBirthday(new Date());
		
		amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
	}
	
}

2、消息接收者

使用@RabbitListener来设定监听队列的方法,方法的参数是接收到实体对象,代码如下:

AmqpReceiver.java

package com.gavinbj.mqprocess.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Message消费者
 * 
 * @author gavinbj
 *
 */
@Component
public class AmqpReceiver {
    
    /**
	 * Topic队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "topic.queue1")
    public void processTopicQueue1(UserInfo  userInfo) {
        System.out.println("Receiver Topic Queue1 : " + userInfo.toString());
    }
    
    /**
	 * Topic队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "topic.queue2")
    public void processTopicQueue2(UserInfo  userInfo) {
        System.out.println("Receiver Topic Queue2 : " + userInfo.toString());
    }
    
}

3、创建Controller进行测试

调用生产者进行消息的发送,消息的接收者收到消息后打印消息内容。

AmqpController.java

package com.gavinbj.mqprocess.controller;

import javax.annotation.Resource;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.AmqpSender;

@RestController
@RequestMapping("/api")
public class AmqpController {

    @Resource
    private AmqpSender sender;
    
    @RequestMapping(value="/messages/topic")
    public BaseResult<String> sendMessageTopic(){
    	
        sender.sendAmqpTopic();
        System.out.println("Topic消息发送成功");
        return ResponseUtils.makeOKRsp("OK");
    }

}

4、启动工程进行测试:

访问地址:http://localhost:9004/gavin/api/messages/topic

返回结果:

{
	"status": 0,
	"code": 1003,
	"msg": "处理成功!",
	"data": "OK"
}

控制台输出结果:

Topic消息发送成功
Receiver Topic Queue2 : UserInfo [Hash = -48188738, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 16:22:34 CST 2020, gender=男, serialVersionUID=1]
Receiver Topic Queue1 : UserInfo [Hash = -48188738, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 16:22:34 CST 2020, gender=男, serialVersionUID=1]

从上可以看出,使用Direct方式消息发送成功,消费者也成功接收到了信息。另外,通过阿里云的控制台也可以看到消息被消费的记录。

13.1.2.4 Fanout Exchange模式消息发送

Fanout路由规则是把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。

阿里云AMQP绑定设定实例如下:

源Exchange 绑定目标类型 目标Queue Binding key
gavinbj.fanout Queue fanout.queue1 user.zhangsan
gavinbj.fanout Queue fanout.queue2 user.

【SpringBoot 2学习笔记】《十三》SpringBoot2消息之旅,带你体验RabbitMQ和Kafka消息发送_第3张图片

说明:生产者P生产消息推送到Exchange,由于Exchange Type=fanout这时候会遵循fanout的规则将消息推送到所有与他绑定的Queue。所以在阿里云的AMQP进行绑定时指定的Key没有意义,但是不指定不让创建绑定。您就在使用时随便写就可以了。

1、创建消息发送者

直接利用上一节中的发送者,添加一个发送Fanout消息的方法,并调用convertAndSend方法进行消息发送,代码如下:

AmqpSender.java

package com.gavinbj.mqprocess.sender;

import java.util.Date;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Message发送者
 * 
 * @author gavinbj
 *
 */
@Component
public class AmqpSender {
	
	@Autowired
	private AmqpTemplate amqpTemplate;

	/**
	 * Direct模式发送消息
	 * 
	 */
	public void sendAmqpDirect() {
		
		String exchange = "gavinbj.direct";
		String routingKey = "direct";
		
		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("gavinbj");
		userInfo.setUserName("黑白猿");
		userInfo.setMobilephone("13940989820");
		userInfo.setEmail("[email protected]");
		userInfo.setIntroduce("欢迎光临");
		userInfo.setGender("男");
		userInfo.setBirthday(new Date());
		
		amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
	}
	
	/**
	 * Topic模式发送消息
	 * 
	 */
	public void sendAmqpTopic() {
		
		String exchange = "gavinbj.topic";
		String routingKey = "topic.zhangsan";
		
		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("gavinbj");
		userInfo.setUserName("黑白猿");
		userInfo.setMobilephone("13940989820");
		userInfo.setEmail("[email protected]");
		userInfo.setIntroduce("欢迎光临");
		userInfo.setGender("男");
		userInfo.setBirthday(new Date());
		
		amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
	}
	
	/**
	 * Fanout模式发送消息
	 * 
	 */
	public void sendAmqpFanout() {
		
		String exchange = "gavinbj.fanout";
		String routingKey = "nouse";
		
		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("gavinbj");
		userInfo.setUserName("黑白猿");
		userInfo.setMobilephone("13940989820");
		userInfo.setEmail("[email protected]");
		userInfo.setIntroduce("欢迎光临");
		userInfo.setGender("男");
		userInfo.setBirthday(new Date());
		
		amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
	}
	
}

2、消息接收者

使用@RabbitListener来设定监听队列的方法,方法的参数是接收到实体对象,代码如下:

AmqpReceiver.java

package com.gavinbj.mqprocess.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Message消费者
 * 
 * @author gavinbj
 *
 */
@Component
public class AmqpReceiver {
	
	/**
	 * Direct队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "direct.queue")
    public void processDirect(UserInfo  userInfo) {
        System.out.println("Receiver Direct: " + userInfo.toString());
    }
    
    /**
	 * Topic队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "topic.queue1")
    public void processTopicQueue1(UserInfo  userInfo) {
        System.out.println("Receiver Topic Queue1 : " + userInfo.toString());
    }
    
    /**
	 * Topic队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "topic.queue2")
    public void processTopicQueue2(UserInfo  userInfo) {
        System.out.println("Receiver Topic Queue2 : " + userInfo.toString());
    }
    
    /**
	 * Fanout队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "fanout.queue1")
    public void processFanoutQueue1(UserInfo  userInfo) {
        System.out.println("Receiver Fanout Queue1: " + userInfo.toString());
    }
    
    /**
	 * Fanout队列消费者
	 * @param userInfo
	 */
    @RabbitListener(queues = "fanout.queue2")
    public void processFanoutQueue2(UserInfo  userInfo) {
        System.out.println("Receiver Fanout Queue2: " + userInfo.toString());
    }
}

3、创建Controller进行测试

调用生产者进行消息的发送,消息的接收者收到消息后打印消息内容。

AmqpController.java

package com.gavinbj.mqprocess.controller;

import javax.annotation.Resource;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.AmqpSender;

@RestController
@RequestMapping("/api")
public class AmqpController {

    @Resource
    private AmqpSender sender;

    @RequestMapping(value="/messages/direct")
    public BaseResult<String> sendMessageDirect(){
    	
        sender.sendAmqpDirect();
        System.out.println("简单消息发送成功");

        return ResponseUtils.makeOKRsp("OK");
    }
    
    @RequestMapping(value="/messages/topic")
    public BaseResult<String> sendMessageTopic(){
    	
        sender.sendAmqpTopic();
        System.out.println("Topic消息发送成功");
        return ResponseUtils.makeOKRsp("OK");
    }
    
    @RequestMapping(value="/messages/fanout")
    public BaseResult<String> sendMessageFanout(){
    	
        sender.sendAmqpFanout();
        System.out.println("Fanout消息发送成功");
        return ResponseUtils.makeOKRsp("OK");
    }

}

4、启动工程进行测试:

访问地址:http://localhost:9004/gavin/api/messages/fanout

返回结果:

{
	"status": 0,
	"code": 1003,
	"msg": "处理成功!",
	"data": "OK"
}

控制台输出结果:

Fanout消息发送成功
Receiver Fanout Queue1: UserInfo [Hash = 546595490, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 21:42:20 CST 2020, gender=男, serialVersionUID=1]
Receiver Fanout Queue2: UserInfo [Hash = 546595490, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 21:42:20 CST 2020, gender=男, serialVersionUID=1]

从上可以看出,使用Fanout方式消息发送成功,消费者也成功接收到了信息。另外,通过阿里云的控制台也可以看到消息被消费的记录。

13.2 Kafka消息队列

首先需要一个Kafka的消息队列,因为阿里云的Kafka消息队列服务比较贵,所以在自己原来的ECS服务器上搭建了一个简单的单机的Kafka消息队列服务,以便我们进行接下来的编码。安装过程参考:【环境部署】华为云ECS安装Kafka消息队列

13.2.1 Kafka消息队列简介

  • Broker
    Kafka集群包含一个或多个服务器,这种服务器被称为broker
  • Topic
    每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic,也就是我们所说的队列。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition
    Partition是物理上的概念,每个Topic包含一个或多个Partition。
  • Producer
    负责发布消息到Kafka broker
  • Consumer
    消息消费者,向Kafka broker读取消息的客户端。
  • Consumer Group
    每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。

13.2.2 SpringBoot2整合Kafka

在SpringBoot2工程中整合Kafka的过程和使用AMQP的过程基本一致。主要分为如下几部分:

加入Kafka依赖

配置Kafka服务信息

编写Kafka的生产者和消费者

13.2.2.1 项目配置

前提:需要有可以访问的Kafka消息服务。您可以购买华为云等服务厂商提供的相应服务,或者自行安装Kafka服务器。

POM文件添加依赖

	
	<dependency>
	    <groupId>org.springframework.kafkagroupId>
	    <artifactId>spring-kafkaartifactId>
	dependency>

在配置文件中配置Kafka生产者和消费者的信息,配置文件内容如下:

application.properties

# Kafka Producer配置
spring.kafka.producer.bootstrap-servers=39.99.177.211:9092

# Kafka Consumer配置
spring.kafka.consumer.bootstrap-servers=39.99.177.211:9092
spring.kafka.consumer.group-id=gavinbj
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-offset-reset=latest
spring.kafka.template.default-topic=users
13.2.2.2 整合Kafka代码

创建一个Kafka消息发送者,使用KafkaTemplate进行消息队列消息的发送。代码清单如下:

KafkaSender.java

package com.gavinbj.mqprocess.sender;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

import com.gavinbj.mqprocess.entity.UserInfo;

/**
 * Kafka消息生产者
 * 
 * @author gavinbjAdministrator
 *
 */
@Component
public class KafkaSender {
	
	@Autowired
	private KafkaTemplate<String, String> kafkaTemplate;

	public void sendKafkaMsg() {
		
		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("gavinbj");
		userInfo.setUserName("黑白猿");
		userInfo.setMobilephone("13940989820");
		userInfo.setEmail("[email protected]");
		userInfo.setIntroduce("欢迎光临");
		userInfo.setGender("男");
		userInfo.setBirthday(new Date());
		
		kafkaTemplate.send("test", userInfo.toString());
		
	}
}

创建一个Kafka消息消费者,通过@KafkaListerner注解来监听对应的主题。当有消息的时候,进行消费。

package com.gavinbj.mqprocess.listener;

import java.util.Optional;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

/**
 * Kafka消息消费者
 * 
 * @author gavinbj
 *
 */
@Component
public class KafkaReceiver {

	@KafkaListener(topics = "test", groupId = "test-consumer-group")
	public void processKafkaMsg(ConsumerRecord<String, String> record) {
		Optional<?> kafkaMsg = Optional.ofNullable(record.value());
		if(kafkaMsg.isPresent()) {
			System.out.println("Kafka Receiver Data:" + record);
			System.out.println("Kafka Receiver Data:" + kafkaMsg.get());
		}
	}
}

创建一个KafkaController进行调用消息发送测试。

package com.gavinbj.mqprocess.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.KafkaSender;

@RestController
@RequestMapping("/api")
public class KafkaController {

	@Autowired
	private KafkaSender kafkaSender;
	
	@GetMapping("/messages/kafka")
	public BaseResult<String> sendKafkaMessage(){
		
		kafkaSender.sendKafkaMsg();
        System.out.println("Kafka消息发送成功");

        return ResponseUtils.makeOKRsp("OK");
	}
	
}
13.2.2.3 运行测试

启动运行后出现如下错误:

Exception thrown when sending a message with key='' and payload='1' to topic test

解决办法:

尝试了网上的各种方法,最后参考这篇文章解决了问题。

修改kafka配置文件server.properties中的如下两项:listeners 和 advertised.listeners。

  • listeners=PLAINTEXT://{内网ip}:9092

  • advertised.listeners=PLAINTEXT://{外网ip}:9092

重新启动本地项目,发送消息成功,我这里将内外网地址直接修改成了ECS服务器的对应IP。修改内容如下:

# The address the socket server listens on. It will get the value returned from
# java.net.InetAddress.getCanonicalHostName() if not configured.
#   FORMAT:
#     listeners = listener_name://host_name:port
#   EXAMPLE:
#     listeners = PLAINTEXT://your.host.name:9092
listeners=PLAINTEXT://172.26.46.219:9092

# Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured.  Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
advertised.listeners=PLAINTEXT://39.99.177.211:9092

[root@iZ8vbf0eo4dqgjfxtrpinfZ config]# vi server.properties 
[root@iZ8vbf0eo4dqgjfxtrpinfZ config]# 
[root@iZ8vbf0eo4dqgjfxtrpinfZ config]# cd ..
[root@iZ8vbf0eo4dqgjfxtrpinfZ kafka_2.13-2.4.0]# bin/kafka-console-producer.sh --broker-list 172.26.46.213:9092 --topic test
>helloe
>

[root@iZ8vbf0eo4dqgjfxtrpinfZ kafka_2.13-2.4.0]# bin/kafka-console-consumer.sh --bootstrap-server 172.26.46.213:9092 --topic test --from-beginning
hello world
helloe
UserInfo [Hash = 1181217845, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Fri Mar 13 17:52:39 CST 2020, gender=男, serialVersionUID=1]

2020-03-13 17:52:24.922  INFO 103732 --- [           main] o.a.coyote.http11.Http11NioProtocol      : Starting ProtocolHandler ["http-nio-9004"]
2020-03-13 17:52:24.940  INFO 103732 --- [           main] com.gavinbj.mqprocess.AmqpApplication    : Started AmqpApplication in 1.834 seconds (JVM running for 2.41)
2020-03-13 17:52:39.587  INFO 103732 --- [nio-9004-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/gavin]  : Initializing Spring DispatcherServlet 'dispatcherServlet'
Kafka消息发送成功
Kafka Receiver Data:ConsumerRecord(topic = test, partition = 0, leaderEpoch = 0, offset = 8, CreateTime = 1584093159876, serialized key size = -1, serialized value size = 207, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = UserInfo [Hash = 1181217845, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Fri Mar 13 17:52:39 CST 2020, gender=男, serialVersionUID=1])
Kafka Receiver Data:UserInfo [Hash = 1181217845, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Fri Mar 13 17:52:39 CST 2020, gender=男, serialVersionUID=1]

从上可以看出,设定后通过命令行发送消息和启动SpringBoot工程发送消息,都能正常进行被接收。

你可能感兴趣的:(SpringBoot2)