MQ全称为Message Queue,即消息队列. 它也是一个队列,遵循FIFO原则。
市面上常见的MQ:
ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。
RabbitMQ官方地址:http://www.rabbitmq.com/
RabbitMQ是基于AMQP协议实现一种Mq.
使用场景:
1、提高系统响应速度
任务异步处理。 将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、提高系统稳定性
系统挂了关系,操作内容放到消息队列.
3、异步化
4、解耦
应用程序解耦合 MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
5、排序保证 FIFO
秒杀
6、消除峰值
好处:提高响应速度、服务异步化、服务之间彻底解耦、消除峰值-把并发请求串行化
RabbitMQ工作原理图
Channel(信道)、Exchange(交换器)、Queue(队列)
安装RabbitMQ:
1、安装Erlang
http://erlang.org/download/otp_win64_20.3.exe
双击:otp_win64_20.2.exe
配置erlang环境变量:
ERLANG_HOME=D:\erl9.2
在path中添 加%ERLANG_HOME%\bin;
2、安装RabbitMQ
RabbitMQ的下载地址:http://www.rabbitmq.com/download.html
双击:rabbitmq-server-3.7.4.exe
安装rabbitMQ的管理插件:
D:\RabbitMQ Server\rabbitmq_server-3.7.4\sbin目录下cmd:rabbitmq-plugins.bat enable rabbitmq_management
启动RabbitMQ
进入浏览器,输入:http://localhost:15672
登录
username:guest
password:guest
RabbitMQ的6种消息模型
3、4、5这三种都属于订阅模型,只不过进行路由的方式不同
导包
Spring Boot默认已集成RabbitMQ
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<!--和springboot2.0.5对应-->
<version>5.4.1</version>
</dependency>
连接工具类
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("127.0.0.1");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
1、HelloWord模型
1个生产者,1个队列,1个消费者。
消息发送方:
import com.lxn.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
//发送
public class Send {
//队列名字
public static final String NAME_QUEUE_HELLOWORD = "helloWord";
public static void main(String[] args) throws Exception {
//连接工具获取连接对象
Connection connection = ConnectionUtil.getConnection();
//创建通道
Channel channel = connection.createChannel();
//使用默认交换机
//创建队列,参数列表(1队列名称,2是否持久化,3队列是否独占此连接,4队列不再使用时是否自动删除此队列,5队列参数)
channel.queueDeclare(NAME_QUEUE_HELLOWORD,true,false,false,null);
//发消息
String msg = "hello word MQ";
/*
param1:Exchange的名称,如果没有指定,则使用默认交换机
param2:routingKey(队列名字),消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
param3:消息包含的属性
param4:消息体
*/
channel.basicPublish("",NAME_QUEUE_HELLOWORD,null,msg.getBytes());
System.out.println("发送成功:"+msg);
}
}
消息接收方:
import com.lxn.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Rev {
//队列名字
public static void main(String[] args) throws Exception {
//连接工具获取连接对象
Connection connection = ConnectionUtil.getConnection();
//创建通道
Channel channel = connection.createChannel();
//使用默认交换机
//创建队列
channel.queueDeclare(Send.NAME_QUEUE_HELLOWORD,true,false,false,null);
//接受消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("收到消息:"+new String(body));
//手动签收,如果是自动签收则不写这行代码
channel.basicAck(envelope.getDeliveryTag(),false);
}
};//autoAck:true自动签收,false手动签收
channel.basicConsume(Send.NAME_QUEUE_HELLOWORD,false, consumer);
}
}
在什么情况下RabbitMQ会丢失消息:
使用自动签收,消费逻辑过程中报异常了,就会导致消息丢失。
解决办法:手动签收,当消息逻辑正常执行完了,再去手动签收。
2、Work_queue模型
1个生产者,1个队列,2个消费者
两个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度
生产者和消费者代码与HelloWord模型代码完全一样。只是多了一个消费者类而已。
消息分配:
1、默认轮询
2、能者多劳
能者多劳配置:在接收方创建队列代码后添加以下代码
//每个消费者同时只处理一个消息
channel.basicQos(1);
3、订阅模型
1、1个生产者,多个消费者
2、每一个消费者都有自己的一个队列
3、生产者没有将消息直接发送到队列,而是发送到了交换机
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
X(Exchanges):
交换机一方面:接收生产者发送的消息。
另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。
到底如何操作,取决于Exchange的类型。
Exchange类型有以下几种:
Fanout:广播,将消息交给所有绑定到交换机的队列 all
Direct:定向,把消息交给符合指定routing key 的队列 一堆或一个
Topic:通配符,把消息交给符合routing pattern(路由模式)的队列 一堆或者一个
订阅模型1(Fanout-广播模式)
消息发送流程是这样的:
- 1) 可以有多个消费者
- 2) 每个消费者有自己的queue(队列)
- 3) 每个队列都要绑定到Exchange(交换机)
- 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 5) 交换机把消息发送给绑定过的所有队列
- 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
消息发送者:
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class Send {
//交换机名字
public static final String NAME_EXCHANGE_FANOUT = "fanout";
public static void main(String[] args) throws Exception {
//连接工具获取连接对象
Connection connection = ConnectionUtil.getConnection();
//创建通道
Channel channel = connection.createChannel();
//创建交换机
channel.exchangeDeclare(NAME_EXCHANGE_FANOUT, BuiltinExchangeType.FANOUT);
//发消息
String msg = "exchange fanout MQ";
channel.basicPublish(NAME_EXCHANGE_FANOUT,"",null,msg.getBytes());
System.out.println("发送成功!"+msg);
}
}
消息接受者:
import com.rabbitmq.client.*;
import java.io.IOException;
public class Rev {
//队列名字
public static final String NAME_QUEUE_FANOUT = "queue_fanout";
public static void main(String[] args) throws Exception {
//连接工具获取连接对象
Connection connection = ConnectionUtil.getConnection();
//创建通道
Channel channel = connection.createChannel();
//使用默认交换机
//创建队列
channel.queueDeclare(NAME_QUEUE_FANOUT,true,false,false,null);
//把队列绑定交换机
channel.queueBind(NAME_QUEUE_FANOUT,Send.NAME_EXCHANGE_FANOUT,"");
//同时只处理一个消息
channel.basicQos(1);
//接受消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("收到消息:"+new String(body));
//手动签收
channel.basicAck(envelope.getDeliveryTag(),false);
}
};//autoAck:true自动签收,false手动签收
channel.basicConsume(NAME_QUEUE_FANOUT,false, consumer);
}
}
import com.rabbitmq.client.*;
import java.io.IOException;
public class Rev1 {
//队列名字
public static final String NAME_QUEUE1_FANOUT = "queue_fanout2";
public static void main(String[] args) throws Exception {
//连接工具获取连接对象
Connection connection = ConnectionUtil.getConnection();
//创建通道
Channel channel = connection.createChannel();
//使用默认交换机
//创建队列
channel.queueDeclare(NAME_QUEUE1_FANOUT,true,false,false,null);
//把队列绑定交换机
channel.queueBind(NAME_QUEUE1_FANOUT,Send.NAME_EXCHANGE_FANOUT,"");
//同时只处理一个消息
channel.basicQos(1);
//接受消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("收到消息:"+new String(body));
//手动签收
channel.basicAck(envelope.getDeliveryTag(),false);
}
};//autoAck:true自动签收,false手动签收
channel.basicConsume(NAME_QUEUE1_FANOUT,false, consumer);
}
}
订阅模型2(Direct-定向模式)
有选择性的接收消息
在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模式下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
同一个接收方可以指定多个RoutingKey。
发送方和接收方代码和广播模式代码完全相同,但是发送方和接收方都要指定routing key
订阅模型3(Topic-通配符模式)
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。
Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
发送方和接收方代码和定向模式代码完全相同,但是接收方指定的routing key要使用通配符
Routingkey 一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: goods.insert
通配符规则:
#:匹配一个或多个词
**:匹配不多不少恰好1个词
举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs*
持久化-解决数据安全
如何避免消息丢失?
1) 消费者的ACK机制。可以防止消费者丢失消息。
2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了
要将消息持久化,前提是:队列、Exchange都持久化
交换机持久化:(发送方代码)
队列持久化:(接收方代码)
消息持久化:(发送方代码)