一:消息队列
“消息队列”是在消息的传输过程中保存消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。
消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。
二:为什么要有消息队列
(1) 通过异步处理提高系统性能(削峰、减少响应所需时间)
如上图,在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。
(2) 降低系统耦合性
我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。
我们最常见的事件驱动架构类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示:
消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。
消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。
另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。
三:RabbitMq
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
RocketMq是一个由阿里巴巴开源的消息中间件,脱胎去阿里每部使用的MetaQ,在设计上借鉴了Kafka。2012年开源,2017年成为apache顶级项目
安装:
-
安装 Erlang
yum install erlang #等待安装完成 #检查是否安装成功 erl -version
-
安装 Rabbitmq-server
yum install rabbitmq-server
-
修改配置文件,修改rabbitmq.config文件(yum安装放在/etc/rabbitmq/)
将配置文件中"%% {loopback_users, []},",这一行的逗号去掉,目的是为了开启远程访问
- 开启Web界面可视化管理插件
rabbitmq-plugins enable rabbitmq_management
- 添加用户
# 添加新用户
rabbitmqctl add_user username password
# 设置用户tag
rabbitmqctl set_user_tags username administrator
# 赋予用户默认vhost的全部操作权限
rabbitmqctl set_permissions -p / username ".*" ".*" ".*"
#查看现有的用户
rabbitmqctl user_list
# 由于RabbitMQ默认的账号用户名和密码都是guest。为了安全起见, 先删掉默认用户
rabbitmqctl delete_user guest
-
操作
# 添加开机启动RabbitMQ服务 chkconfig rabbitmq-server on # 启动服务 service rabbitmq-server start # 查看服务状态 $service rabbitmq-server status # 停止服务 service rabbitmq-server stop
-
查看web
特点:
- 保证可靠性( Reliabil ity ) 。RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认等。
- 具有灵活的路由( Flex ible Routing )功能。在消息进入队列之前,是通过Exchange (交换器)来路由消息的。对于典型的路由功能, RabbitMQ 已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange 绑定在一起,也可以通过插件机制来实现自己的Exchange 。
- 支持消息集群( Clustering ) 。多台RabbitMQ 服务器可以组成一个集群,形成一个逻辅
Broker 。 - 具有高可用性( H ighly Available ) 。队列可以在集群中的机器上进行镜像,使得在部分节点出现问题的情况下队列仍然可用。支持多种协议( Multi-protocol ) 。RabbitMQ 除支持AMQP 协议之外,还通过插件的方式支持其他消息队列协议,比如STOMP, MQTT 等。
- 支持多语言客户端( Many Client )。RabbitMQ 几乎支持所有常用的语言,比如Java 、.NET 、Ruby 等
- 提供管理界面( Management UI ) 。RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息Broker 的许多方面
- 提供跟踪机制( Trac ing ) 。RabbitMQ 提供了消息跟踪机制,如果消息异常,使用者可以查出发生了什么情况。
- 提供插件机制( Plugin System ) 。RabbitMQ 提供了许多插件,从多方面进行扩展,也可以编写自己的插件。
RabbitMQ 基本概念:
- Message (消息) : 消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列可选属性组成,这些属性包括routing-key (路由键)、priority (相对于其他消息的优先级〉、delivery-mode(指出该消息可能需要持久化存储)等。
- Publisher (消息生产者) : 一个向交换器发布消息的客户端应用程序。
- Exchange (交换器):用来接收生产者发送的消息,并将这些消息路由给服务器中的队列。
- Binding (绑定) : 用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
- Queue (消息队列):用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一条消息可被投入一个或多个队列中。消息一直在队列里面,等待消费者连接到这个队列将其取走。
- Connection (网络连接〉:比如一个TCP 连接。
- Channel ( 信道): 多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP 连接内的虚拟连接, AMQP 命令都是通过信道发送出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成的。因为对于操作系统来说, 建立和销毁TCP 连接都是非常昂贵的开销,所以引入了信道的概念,以复用一个TCP连接。
- Consumer (消息消费者) : 表示一个从消息队列中取得消息的客户端应用程序。
- Virtual Host (虚拟主机, 在RabbitMQ 中Qlj vhosD : 表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。本质上每个vhost就是一台缩小版的RabbitMQ 服务器,它拥有自己的队列、交换器、绑定和权限机制。
- vhost 是AMQP 概念的基础,必须在连接时指定, RabbitMQ 默认的vhost 是“/”。
- Broker : 表示消息队列服务器实体。
ARabbitMq为什么用信道?不用TCP直接通信
- TCP的创建和销毁开销很大,创建需要3次握手,销毁需要4次分手
- 如果不用信道,应用会以TCP连接rabbitmq,数据高峰的时候,千万条数据会造成巨大浪费而且操作系统处理tcp连接能力也有限制,必定会造成性能上的瓶颈
- 信道原理,是一个线程一个信道,多个线程多个信道同用一条tcp链接,一条TCP连接可以容纳无限的信道,即使mm每秒成千上万条请求也不会有性能上的瓶颈
AMQP 中的消息路由
AMQP 中的消息路由过程和Java 开发者熟悉的JMS 存在一些差别,在AMQP 中增加了Exchan ge 和B inding 的角色。生产者需要把消息发布到Exchange 上,消息最终到达队列并被消费者接收,而Binding 决定交换器上的消息应该被发送到哪个队列中
交换器类型
不同类型的交换器分发消息的策略也不同,目前交换器有4 种类型: Direct 、Fanout、Topic 、Headers 。其中Headers 交换器匹配AMQP 消息的Header 而不是路由键。此外, Headers 交换器和Direct 交换器完全一致,但性能相差很多, 目前几乎不用了,所以下面我们看另外三种类型。
一:Direct交换器:直接根据路由键(rounting-key)来指定转发给哪个队列处理,direct交换器是完全匹配式
Provier实现:
- 添加rabbitmq的坐标
-
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.whut</groupId> <artifactId>spring-boot-direct-provider</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!--rabbitmq坐标--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> </dependencies> </project>
- 配置yml文件
server: port: 8080 #配置端口号 #port: ${random.int[1024,9999]} #生成随机端口号 spring: mvc: view: prefix: / suffix: .html servlet: #配置文件大小 multipart: max-file-size: 20Mb max-request-size: 20Mb #rabbitmq配置 rabbitmq: host: yor_port port: 5672 username: root password: 123456 #配置交换器名字 mq: config: exchange: log.direct routing: info: key: log.info.routing.key error: key: log.error.routing.key
- 添加sender
-
@Component public class Sender { @Autowired private AmqpTemplate amqpTemplate; @Value("${mq.config.exchange}") private String exchangeName; @Value("${mq.routing.info.key}") private String routingKey; public void send(String msg){ /** * 转发消息 */ amqpTemplate.convertAndSend(this.exchangeName,this.routingKey,msg); } }
- 测试代码
-
@RunWith(SpringRunner.class) @SpringBootTest(classes = SpringBootServerApplication.class) public class QueueTest { @Autowired private Sender sender; @Test public void test1() throws InterruptedException { int i = 0; while (true){ Thread.sleep(1000); System.out.println("i ="+ i); this.sender.send("sender "+ i++); } } }
Reciver实现:
- pom文件配置同上
- 修改yml文件
server:
port: 8080 #配置端口号
#port: ${random.int[1024,9999]} #生成随机端口号
spring:
mvc:
view:
prefix: /
suffix: .html
servlet: #配置文件大小
multipart:
max-file-size: 20Mb
max-request-size: 20Mb
#rabbitmq配置
rabbitmq:
host: *************
port: 5672
username: root
password: 123456
#配置交换器名字
mq:
config:
exchange: log.direct
#配置队列信息
queue:
info: log.info
error: log.error
routing:
info:
key: log.info.routing.key
error:
key: log.error.routing.key
- 编写服务消费者Reciver
package com.whut.receiver;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.lang.model.type.ExecutableType;
@Component
/**
* bindings:绑定队列
* @QueueBinding
* value:配置队列名称
* exchange:配置交换器
* key:配置路由键
* @Queue autoDelete:配置是否为临时队列,当配置为true时候为临时队列,即当消费者蹦的时候,再次启动时候会
* 丢失数据
* value:配置队列名称
*
* @Exchange value:配置交换器名字
* type 配置交换器的模式
*
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "${mq.queue.info}",autoDelete = "true"),
exchange = @Exchange(value = "${mq.config.exchange}",type = ExchangeTypes.DIRECT),
key = "${mq.routing.info.key}"
)
)
public class InfoReceiver {
@RabbitHandler
public void receiver(String msg){
System.out.println("InfoReceiver ----> " + msg);
}
}
测试:
二:Topic交换器:通过模糊匹配来匹配相应的队列
Provider实现:
- 同上在yml修改exchange交换器的名字
- Sender同上
@Component
public class UserSender {
@Autowired
private AmqpTemplate amqpTemplate;
@Value("${mq.config.exchange}")
private String exchangeName;
public void send(String msg){
/**
* 转发消息
*/
amqpTemplate.convertAndSend(this.exchangeName,"user.log.debug","user.log.debug = " + msg);
amqpTemplate.convertAndSend(this.exchangeName,"user.log.info","user.log.info = " + msg);
amqpTemplate.convertAndSend(this.exchangeName,"user.log.warn","user.log.warn =" +msg);
amqpTemplate.convertAndSend(this.exchangeName,"user.log.error","user.log.error =" + msg);
}
}
- 测试代码同上
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBootServerApplication.class)
public class QueueTest {
@Autowired
private UserSender userSender;
@Autowired
private ProductSender productSender;
@Autowired
private OrderSender orderSender;
@Test
public void test1() throws InterruptedException {
while (true){
Thread.sleep(1000);
this.userSender.send("UserSender ...............");
this.orderSender.send("orderSender ...............");
this.productSender.send("productSender ...............");
}
}
}
Consumer实现:
- 同上在yml文件中修改exchange交换器的名字
- 修改reciver的相应配置,如key和匹配type
@Component
/**
* bindings:绑定队列
* @QueueBinding
* value:配置队列名称
* exchange:配置交换器
* key:配置路由键
* @Queue autoDelete:配置是否为临时队列,当配置为true时候为临时队列,即当消费者蹦的时候,再次启动时候会
* 丢失数据
* value:配置队列名称
*
* @Exchange value:配置交换器名字
* type 配置交换器的模式
*
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "${mq.queue.info}",autoDelete = "true"),
exchange = @Exchange(value = "${mq.config.exchange}",type = ExchangeTypes.TOPIC),
key = "*.log.info"
)
)
public class InfoReceiver {
@RabbitHandler
public void receiver(String msg){
System.out.println("InfoReceiver ----> " + msg);
}
}
实现效果:
二:Fanout交换器:Fanout 交换器不处理路由键,只是简单地将队列绑定到交换器,发送到交换器的每条消息都会被转发到与该交换器绑定的所有队列中。这很像子网广播,子网内的每个主机都获得了一份复制的消息。通过Fanout 交换器转发消息是最快的。
实现略。