在分布式系统中,消息服务是不可或缺的重要部分,如异步处理、流量削峰、分布式解耦、分布式事务管理等,使用消息服务可以实现一个高性能、高可用、高扩展的系统。
常见的消息中间件:
在没有特别要求的情况下,通常会选择RabbitMQ作为消息中间件,如果针对的是大数据业务,推荐使用Kafka或者RocketMQ作为消息中间件。
RabbitMQ是基于AMQP1协议的轻量级、可靠、可伸缩和可移植的消息代理,Spring Boot中对RabbitMQ进行了集成管理。
该工作模式不需要设置交换器,RabbitMQ会使用内部默认的交换器进行消息转换,需要指定唯一的消息队列进行消息传递,可以有多个消息消费者,多个消费者通过轮询方式依次接收消息队列中存储的消息。
适用于较为繁重,并且可以拆分处理的业务。
该模式必须先配置一个fanout类型的交换器,不需要指定对应的路由键,同时会将消息路由到每一个消息队列上,然后每个消息队列都可以对相同的消息进行接收存储,进而由各自消息队列关联的消费者进行消费。
适用于进行相同业务功能处理的场合,例如用户注册后发送短信和邮件通知,那么邮件服务消费者和短信服务消费者需要共同消费“用户注册成功”这一条消息。
该模式必须先配置一个direct类型的交换器,并指定不同的路由键值将对应消息从交换器路由到不同的消息队列中进行存储,再由消费者进行各自消费。
适用于进行不同类型消息分类处理的场合,例如日志收集处理,用户可以配置不同的路由键值分别对不同级别的日志信息进行分类处理。
该模式必须先配置一个topic类型的交换器,并指定不同的路由键值,将对应的消息从交换器路由到不同的消息队列进行存储,然后由消费者进行各自消费。与Routing不同的是,Topics模式设置的路由键是包含通配符的,#匹配多个字符,*匹配一个字符,然后与其他字符一起使用“.”进行连接,从而组成动态路由键。
适用于根据不同需求动态传递处理业务的场合,例如一些订阅客户只接收邮件消息,一些订阅客户直接收短信消息,那么可以很具客户需求进行动态路由匹配,从而将订阅消息分发到不同的消息队列中。
该模式不需要设置交换器,需要制定唯一的消息队列进行消息传递,与Work queues工作模式相似,不同在于RPC模式是一个回环结构,主要针对分布式架构的消息传递业务,客户端先发送消息到消息队列,远程服务端获取消息,然后再写入另一个消息队列,向原始的客户端响应消息处理结果。
适用于远程服务调用的业务处理场合,例如在分布式架构中必须考虑的分布式事务管理问题。
较为少用,该模式必须设置一个headers类型的交换器,不需要设置路由键,取而代之的实在Properties属性配置中的headers头信息中使用key/value的形式配置路由规则。
RabbitMQ官网:https://www.rabbitmq.com/
以3.7.9版本为例,下载地址:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.9,找到“rabbitmq-server-3.7.9.exe”进行下载,还需下载对应版本的Erlang语言包Erlang 21.0,地址:https://www.erlang.org/patches/otp-21.0。
RabbitMQ版本 和 Erlang 版本关系:https://www.rabbitmq.com/which-erlang.html
下载完成后,先安装Erlang语言包,再安装RabbitMQ安装包。首次安装完成后,系统环境变量中会出现ERLANG_HOME
的变量,配置的时Erlang的安装路径。可以在path中新增%ERLANG_HOME%\bin
,这样在cmd输入erl
可查看Erlang版本信息。
如果你是第一次安装,那么安装成功后会自动创建 RabbitMQ 服务并启动。
rabbitmqctl status //查看当前状态
rabbitmq-server start //启动服务
rabbitmq-server stop //停止服务
rabbitmq-server restart //重启服务
rabbitmq-plugins enable rabbitmq_management //开启Web插件
RabbitMQ默认提供两个端口号,5672用作服务端口号,15672用作可视化管理端口号。浏览器上通过http://localhost:15672
查看可视化RabbitMQ,默认登录账号密码为guest。
可能会遇到可视化页面无法打开的情况,进入rabbitmq目录的sbin目录下,执行
rabbitmq-plugins enable rabbitmq_management 即可
引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
application.yml配置:
spring:
#RabbitMQ消息中间件连接配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#配置RabbitMQ虚拟主机路径/,默认可以省略
virtual-host: /
注:Spring Boot中也集成了一个内部默认的RabbitMQ中间件,如果没有在配置文件中配置外部RabbitMQ连接,会启动内部的RabbitMQ中间件,这种内部的RabbitMQ中间件是不推荐使用的。
Spring Boot整合RabbitMQ中间件实现消息服务主要围绕三个部分:定制中间件、消息发送者发送消息、消息消费者接收消息。
以注册账号成功后发送邮件和短信为例:
(1) 主要通过org.springframework.amqp.core.AmqpAdmin类定制消息发送组件:
package com.xc.controller;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wyp
*/
@RestController
public class TestController {
@Autowired
private AmqpAdmin amqpAdmin;
@GetMapping("/amqpAdmin")
public String customComponents() {
//定义fanout类型的交换器
amqpAdmin.declareExchange(new FanoutExchange("fanout_exchange"));
//定义两个默认持久化队列,分别处理邮件和短信
amqpAdmin.declareQueue(new Queue("fanout_queue_email"));
amqpAdmin.declareQueue(new Queue("fanout_queue_sms"));
//将队列分别与交换器进行绑定
amqpAdmin.declareBinding(new Binding("fanout_queue_email",Binding.DestinationType.QUEUE,"fanout_exchange","",null));
amqpAdmin.declareBinding(new Binding("fanout_queue_sms",Binding.DestinationType.QUEUE,"fanout_exchange","",null));
return "OK";
}
}
执行该方法后在可视化界面的Exchanges面板如下:
Queues面板查看定制生成的消息队列信息:
(2) 消息发送者发送消息
先创建一个实体类User
@Data
public class User implements Serializable {
private Integer id;
private String username;
}
解决消息中间件发送实体类消息出现异常一般有两种解决方案:一是实现JDK自带的Serializable序列化接口;二是定制其他类型的消息转化器。第一种可视化效果差,一般使用第二种方式,如下:
package com.xc.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
@Configuration
public class RabbitmqConfig {
/**
* 定义一个Jackson2JsonMessageConverter类型的消息转换组件
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
使用RabbitTemplate模板类实现消息发送:
package com.xc.controller;
import com.xc.entity.User;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wyp
*/
@RestController
public class InfoController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg")
public String sendMsg() {
User user = new User();
user.setId(1);
user.setUsername("张三");
rabbitTemplate.convertAndSend("fanout_exchange","",user);
return "消息发送成功";
}
}
执行sendMsg()方法后如下:
由于没有定义消息消费者接收,所以会把消息暂存在队列中。
(3) 消费者接收消息
使用@RabbitListener注解监听队列消息后,一旦服务启动且监听到指定的队列中有消息存在,对应注解的方法会立即接收并消费队列中的消息。在接收消息的方法中,参数类型可以与发送的消息类型保持一致或者使用Object类型和Message类型。
package com.xc.service;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
/**
* @author wyp
*/
@Service
public class RabbitmqService {
@RabbitListener(queues = "fanout_queue_email")
public void receiveMsgEmail(Message message) {
byte[] body = message.getBody();
String s = new String(body);
System.out.println("邮件业务接收到消息:" + s);
}
@RabbitListener(queues = "fanout_queue_sms")
public void receiveMsgSms(Message message) {
byte[] body = message.getBody();
String s = new String(body);
System.out.println("短信业务接收到消息:" + s);
}
}
重启项目后控制台输出:
短信业务接收到消息:{"id":1,"username":"张三"}
邮件业务接收到消息:{"id":1,"username":"张三"}
使用@Configuration注解配置类定制消息发送组件:
package com.xc.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author wyp
*/
@Configuration
public class RabbitmqConfig {
/**
* 定义一个Jackson2JsonMessageConverter类型的消息转换组件
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 1.定义fanout类型的交换器
*/
@Bean
public Exchange fanoutExchange() {
return ExchangeBuilder.fanoutExchange("fanout_exchange").build();
}
/**
* 2.定义两个不同名称的消息队列
*/
@Bean
public Queue fanoutQueueEmail() {
return new Queue("fanout_queue_email");
}
@Bean
public Queue fanoutQueueSms() {
return new Queue("fanout_queue_sms");
}
/**
* 3.将两个不同名称的消息队列与交换器绑定
*/
@Bean
public Binding bindingEmail() {
return BindingBuilder.bind(fanoutQueueEmail()).to(fanoutExchange()).with("").noargs();
}
@Bean
public Binding bindingSms() {
return BindingBuilder.bind(fanoutQueueSms()).to(fanoutExchange()).with("").noargs();
}
}
之后通过API的方式使用RabbitTemplate模板类实现消息发送,使用@RabbitListener注解监听队列消息即可。
使用@RabbitListener注解属性bindings属性创建并绑定交换器和消息队列组件,想要接收到User实体类必须将交换器类型设置为fanout。
package com.xc.service;
import com.xc.entity.User;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
/**
* @author wyp
*/
@Service
public class RabbitmqService {
@RabbitListener(bindings = @QueueBinding(value = @Queue("fanout_queue_email")
,exchange = @Exchange(value = "fanout_exchange",type = "fanout")))
public void receiveMsgEmail(User user) {
System.out.println("邮件业务接收到消息:" + user);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue("fanout_queue_sms")
,exchange = @Exchange(value = "fanout_exchange",type = "fanout")))
public void receiveMsgSms(User user) {
System.out.println("短信业务接收到消息:" + user);
}
}
之后通过API的方式使用RabbitTemplate模板类实现消息发送即可。
总结:
基于API的方式相对简单直观,但容易与业务代码产生耦合;基于配置类的方式相对隔离,容易统一管理;基于注解的方式清晰明了,方便各自管理,但也容易与业务代码产生耦合。
在实际开发中,使用基于配置类方式和基于注解凡是定制组件实现消息服务较为常见。
以不同级别日志信息采集处理为例:
使用基于注解方式定制消息组件和消费者:
@RabbitListener(bindings = @QueueBinding(value = @Queue("routing_queue_error")
,exchange = @Exchange(value = "routing_exchange",type = "direct")
,key = "error_routing_key"))
public void routingMsgError(String message) {
System.out.println("error级别日志接收到消息:" + message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue("routing_queue_all")
,exchange = @Exchange(value = "routing_exchange",type = "direct")
,key = {"error_routing_key","info_routing_key","warning_routing_key"}))
public void routingMsgAll(String message) {
System.out.println("error,info,warning级别日志接收到消息:" + message);
}
Routing模式下的交换器类型type属性为direct,必须指定key属性,每个消息队列可以映射多个路由键。
启动项目后,如下图
消息发送者实现消息发送:
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendLog")
public String sendLog() {
//convertAndSend(exchange,routingKey,object)
rabbitTemplate.convertAndSend("routing_exchange","error_routing_key","ERROR MESSAGE");
return "消息发送成功";
}
执行上述方法后控制台输出:
error级别日志接收到消息:ERROR MESSAGE
error,info,warning级别日志接收到消息:ERROR MESSAGE
总结:
在Routing工作模式下发送消息时,必须指定路由键参数,该参数要与消息队列映射的路由键保持一致,否则发送的消息将会丢失。
以不同用户对邮件和短信的订阅需求为例:
使用基于注解方式定制消息组件和消费者:
@RabbitListener(bindings = @QueueBinding(value = @Queue("topic_queue_email")
,exchange = @Exchange(value = "topic_exchange",type = "topic")
,key = "info.#.email.#"))
public void topicMsgEmail(String message) {
System.out.println("邮件订阅接收到消息:" + message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue("topic_queue_sms")
,exchange = @Exchange(value = "topic_exchange",type = "topic")
,key = "info.#.sms.#"))
public void topicMsgSms(String message) {
System.out.println("短信订阅接收到消息:" + message);
}
Topics通配符模式与Routing路由模式使用基本一样,主要是将交换器类型type修改为topic,然后使用通配符的样式自己顶路由键key,用点连接。
启动项目后如下图:
消息发送者实现消息发送:
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendSub")
public String sendSub() {
//rabbitTemplate.convertAndSend("topic_exchange","info.email","邮件订阅");
rabbitTemplate.convertAndSend("topic_exchange","info.email.sms","邮件订阅+短信订阅");
return "消息发送成功";
}
执行上述方法后控制台输出:
邮件订阅接收到消息:邮件订阅+短信订阅
短信订阅接收到消息:邮件订阅+短信订阅
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。 ↩︎