MQ就是消息队列,通过典型的生产者和消费者模型,生产者不停的向消息队列中生产消息,消费者不停的从消息队列中获取消息。因为消息的生产与消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松实现系统间解耦合。
**ActiveMQ:**是Apache出品的,它是一个完全支持JMS规范的消息中间件,丰富的API,多种集群架构模式让ActiveMQ成为业界老牌的消息中间件,在中小型企业颇受欢迎。但是它的吞吐量低,性能不高。
**Kafka:**也是Apzche出品的,主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的是用于日志收集与传输,不支持事务,对于消息的重复、丢失、错误没有严格的要求。适合产生大量数据的互联网服务的数据收集业务。
**RocketMQ:**是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路源于Kafka,但它并没有进行copy,它对消息的可靠传输及事务做了优化。
**RabbitMQ:**是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现,AMQP主要特征是面向消息、队列、路由(包括点对点、发布订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性、可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
1、docker pull rabbitmq:3-management(3-management表示带web管理界面的)
2、运行容器
#5672是服务端口;15672是web页面的访问端口,如果不设置用户名密码,默认是guest;25672是集群端口
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq01 \
-e RABBITMQ_DEFAULT_USER=xxx \
-e RABBITMQ_DEFAULT_PASS=xxx \
rabbitmq:3-management
3、在浏览器栏访问IP地址:15672 可以访问到RabbitMQ的登录官网。
没有exchange交换机,一个生产者对应一个消费者,直接将消息放入消息队列,消费者一旦监听到队列中有消息,就会去消费。
**缺点:**如果消费者处理过慢,就会导致消息的堆积。
public class RabbitMQUtils {
public static Connection getConnection(){
try {
//创建mq的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接mq的主机
connectionFactory.setHost("xx.xx.xx.xx");
//设置端口号
connectionFactory.setPort(5672);
//设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名与端口号
connectionFactory.setUsername("ems");
connectionFactory.setPassword("xxx");
return connectionFactory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void closeConnectionAndChannel(Connection connection, Channel channel){
try {
if (channel != null){
channel.close();
}
if (connection != null){
connection.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class Provider {
/**
* 生产消息
*/
@Test
public void sendMessage() throws IOException, TimeoutException {
//获取连接对象
Connection connection = RabbitMQUtils.getConnection();
//获取连接中的通道
Channel channel = connection.createChannel();
/**
* 通道绑定对应的消息队列
* 参数一:队列名称
* 参数二:队列是否持久化,如果为true,当RabbitMQ重启过后,队列不会被清除,但是队列中的消息会被清除
* 参数三:队列是否独占通道
* 参数四:是否在消费完队列并且断开连接后,将队列删除
* 参数五:附加参数
*/
channel.queueDeclare("hello",true,false,false,null);
/**
* 发送消息
* 参数一:交换机
* 参数二:队列的名称
* 参数三:传递消息额外设置,MessageProperties.PERSISTENT_TEXT_PLAIN表示队列中的消息也进行持久化
* 参数四:消息的具体内容
*/
channel.basicPublish("","hello", MessageProperties.PERSISTENT_TEXT_PLAIN,"hello rabbitmq,我来了".getBytes());
//关闭通道、连接
RabbitMQUtils.closeConnectionAndChannel(connection,channel);
}
}
/**
* 不要用Junit测试来实现消息消费,因为不支持多线程
*/
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接对象
Connection connection = RabbitMQUtils.getConnection();
//获取连接中的通道
Channel channel = connection.createChannel();
//通道绑定对应的消息队列
channel.queueDeclare("hello",true,false,false,null);
/**
* 消费消息
* 参数一:队列名称
* 参数二:开启消息的确认机制
* 参数三:消费时的回调函数
*/
channel.basicConsume("hello",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费的消息:"+new String(body));
}
});
}
}
没有交换机,一个生产者对应多个消费者,默认是平均分配的形式来进行消费。
缺点:(默认是采用自动确认机制)如果一个消费者消费过慢也会导致整体效率的低下。
public class Provider {
@Test
public void sendMessage() throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//绑定队列
channel.queueDeclare("work",true,false,false,null);
for (int i = 0; i < 10; i++) {
//生产消息
channel.basicPublish("","work", null,(i + "你好,work queue").getBytes());
}
RabbitMQUtils.closeConnectionAndChannel(connection,channel);
}
}
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work",true,false,false,null);
channel.basicConsume("work",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("消费者1:"+new String(body));
}
});
}
}
消费者1:1你好,work queue
消费者1:3你好,work queue
消费者1:5你好,work queue
消费者1:7你好,work queue
消费者1:9你好,work queue
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work",true,false,false,null);
channel.basicConsume("work",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2:"+new String(body));
}
});
}
}
消费者2:2你好,work queue
消费者2:4你好,work queue
消费者2:6你好,work queue
消费者2:8你好,work queue
消费者2:10你好,work queue
自动确认机制一般不用,因为消息一旦经消费者确认过后,如果此时消费者宕机了,那么原来确认过的消息还未被消费,却已经丢失了。
手动确认机制可以达到能者多劳的地步。
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道中只允许一个消息存在
channel.basicQos(1);
channel.queueDeclare("work",true,false,false,null);
channel.basicConsume("work",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("消费者1:"+new String(body));
//开启手动确认消息
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
消费者1:2你好,work queue
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道中只允许一个消息存在
channel.basicQos(1);
channel.queueDeclare("work",true,false,false,null);
channel.basicConsume("work",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2:"+new String(body));
//开启手动确认消息
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
消费者2:1你好,work queue
消费者2:3你好,work queue
消费者2:4你好,work queue
消费者2:5你好,work queue
消费者2:6你好,work queue
消费者2:7你好,work queue
消费者2:8你好,work queue
消费者2:9你好,work queue
消费者2:10你好,work queue
fanout模型。
- 可以有多个消费者。
- 每一个消费者都有自己的queue。
- 每个queue都要绑定到exchange。
- 生产者发送的消息只能发送到exchange,exchange决定把消息发给哪个queue。
- exchange会把消息发送给所有绑定的queue。
- queue对应的消费者都能够拿到消息。实现一条消息被多个消费者消费。
public class Provider {
@Test
public void sendMessage() throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//将通道声明指定的交换机
//第一个参数是交换机的名字,第二个参数是交换机的类型
channel.exchangeDeclare("logs","fanout");
/**
* 发送消息
* 第一个参数是交换机的名字
* 第二个参数是路由key(fanout模型中写了也没用)
* 第三个参数是队列中的消息进行持久化
* 第四个参数是消息的内容
*/
channel.basicPublish("logs","", MessageProperties.PERSISTENT_TEXT_PLAIN,"我是 fanout queue".getBytes());
RabbitMQUtils.closeConnectionAndChannel(connection,channel);
}
}
每一个消费者代码都一样
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs","fanout");
//建立临时队列,避免浪费资源
String queueName = channel.queueDeclare().getQueue();
//队列绑定交换机
//第三个参数路由key(在fanout模型中没有用)
channel.queueBind(queueName,"logs","");
//消费消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
消费者1:我是 fanout queue
消费者2:我是 fanout queue
消费者3:我是 fanout queue
Direct直连模型:
- 队列与交换机的绑定,不再是任意绑定了,而是要指定一个
RountingKey
。- 消息在向Exchange发送的时候也要指定
RountingKey
。- Exchange不再是直接把消息交给每一个队列,而是根据消息的
RountingKey
进行判断,只有队列的RountingKey
与消息的RountingKey
完全一致,才会接收到消息。
public class Provider {
@Test
public void sendMessage() throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//将通道声明指定的交换机;第一个参数是交换机的名字,第二个参数是交换机的类型
channel.exchangeDeclare("logs_direct","direct");
//定义路由key
String rountingKey = "info";
/**
* 发送消息
* 第一个参数是交换机的名字
* 第二个参数是路由key(fanout模型中写了也没用)
* 第三个参数是队列中的消息进行持久化
* 第四个参数是消息的内容
*/
channel.basicPublish("logs_direct",rountingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,("我是 direct queue,我的rountingKey是:"+rountingKey).getBytes());
RabbitMQUtils.closeConnectionAndChannel(connection,channel);
}
}
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_direct","direct");
//建立临时队列,避免浪费资源
String queueName = channel.queueDeclare().getQueue();
//队列绑定交换机,以及路由key(可以绑定多个RountingKey)
channel.queueBind(queueName,"logs_direct","info");
channel.queueBind(queueName,"logs_direct","warn");
channel.queueBind(queueName,"logs_direct","error");
//消费消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
消费者1:我是 direct queue,我的rountingKey是:info
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_direct","direct");
//建立临时队列,避免浪费资源
String queueName = channel.queueDeclare().getQueue();
//队列绑定交换机,以及路由key
channel.queueBind(queueName,"logs_direct","error");
//消费消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2:" + new String(body));
}
});
}
}
消费者2没有任何打印,说明没有收到任何消息。
topic实际上就比direct模型多了个通配符的功能。
- ==*==匹配一个单词。
- ==#==匹配多个单词。
public class Provider {
@Test
public void sendMessage() throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//将通道声明指定的交换机;第一个参数是交换机的名字,第二个参数是交换机的类型
channel.exchangeDeclare("logs_topic","topic");
//定义路由key
String rountingKey = "user.save";
/**
* 发送消息
* 第一个参数是交换机的名字
* 第二个参数是路由key(fanout模型中写了也没用)
* 第三个参数是队列中的消息进行持久化
* 第四个参数是消息的内容
*/
channel.basicPublish("logs_topic",rountingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,("我是 topic queue,我的rountingKey是:"+rountingKey).getBytes());
RabbitMQUtils.closeConnectionAndChannel(connection,channel);
}
}
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_topic","topic");
//建立临时队列,避免浪费资源
String queueName = channel.queueDeclare().getQueue();
//队列绑定交换机,以及路由key
channel.queueBind(queueName,"logs_topic","user.*");
//消费消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
消费者1:我是 topic queue,我的rountingKey是:user.save
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_topic","topic");
//建立临时队列,避免浪费资源
String queueName = channel.queueDeclare().getQueue();
//队列绑定交换机,以及路由key
channel.queueBind(queueName,"logs_topic","user.*.*");
//消费消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者:2" + new String(body));
}
});
}
}
消费者2没有任何打印,说明没有收到任何消息。
1、pom依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2、yaml文件
spring:
application:
name: rabbitmq-springboot
rabbitmq:
host: xx.xxx.xxx.xx
port: 5672
username: guest
password: guest
virtual-host: /ems # 虚拟主机地址
1、生产者
/**
* 简单模型
*/
@Test
public void simpleQueueSendMessage(){
rabbitTemplate.convertAndSend("hello","你好,我是 hello queue");
}
2、消费者
@Component
//消费者声明一个hello队列,以后就会监听这个队列
@RabbitListener(queuesToDeclare = @Queue(value = "hello",durable = "true",autoDelete = "false"))
public class Consumer {
//消费者接收到消息后的回调函数
@RabbitHandler
public void receiveMessage(String message){
System.out.println("message:"+message);
}
}
你好,我是 hello queue
1、生产者
/**
* work模型
*/
@Test
public void workQueueSendMessage(){
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("work","你好,我是 work queue");
}
}
2、消费者
@Component
public class WorkConsumer {
//如果想要声明多个消费者,就把@RabbitHandler换成@RabbitListener加在方法上面
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive01(String message){
System.out.println("第一个消费者接收到的message:"+message);
}
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive02(String message){
System.out.println("第二个消费者接收到的message:"+message);
}
}
第二个消费者接收到的message:你好,我是 work queue
第一个消费者接收到的message:你好,我是 work queue
第一个消费者接收到的message:你好,我是 work queue
第二个消费者接收到的message:你好,我是 work queue
第一个消费者接收到的message:你好,我是 work queue
第二个消费者接收到的message:你好,我是 work queue
第一个消费者接收到的message:你好,我是 work queue
第二个消费者接收到的message:你好,我是 work queue
第一个消费者接收到的message:你好,我是 work queue
第二个消费者接收到的message:你好,我是 work queue
1、生产者
/**
* fanout模型
*/
@Test
public void fanoutQueueSendMessage(){
rabbitTemplate.convertAndSend("logs_fanout","","你好,我是 fanout queue");
}
2、消费者
@Component
public class FanoutConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //不指定队列名相当于是一个临时队列
exchange = @Exchange(value = "logs_fanout",type = "fanout") //绑定交换机
)
})
public void receive01(String message){
System.out.println("第一个消费者接收到的message:"+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //不指定队列名相当于是一个临时队列
exchange = @Exchange(value = "logs_fanout",type = "fanout") //绑定交换机
)
})
public void receive02(String message){
System.out.println("第二个消费者接收到的message:"+message);
}
}
第一个消费者接收到的message:你好,我是 fanout queue
第二个消费者接收到的message:你好,我是 fanout queue
1、生产者
/**
* 路由direct模型
*/
@Test
public void directQueueSendMessage(){
rabbitTemplate.convertAndSend("logs_direct","info","你好,我是 路由direct queue");
}
2、消费者
@Component
public class DirectConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //不指定队列名相当于是一个临时队列
exchange = @Exchange(value = "logs_direct",type = "direct"), //绑定交换机
key = {
"info","warn","error"} //设置路由key
)
})
public void receive01(String message){
System.out.println("第一个消费者接收到的message:"+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //不指定队列名相当于是一个临时队列
exchange = @Exchange(value = "logs_direct",type = "direct"), //绑定交换机
key = {
"error"} //设置路由key
)
})
public void receive02(String message){
System.out.println("第二个消费者接收到的message:"+message);
}
}
第一个消费者接收到的message:你好,我是 路由direct queue
- ==*==匹配一个单词。
- ==#==匹配多个单词。
1、生产者
/**
* 路由direct模型
*/
@Test
public void directQueueSendMessage(){
rabbitTemplate.convertAndSend("logs_direct","info","你好,我是 路由direct queue");
}
2、消费者
@Component
public class TopicConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //不指定队列名相当于是一个临时队列
exchange = @Exchange(value = "logs_topic",type = "topic"), //绑定交换机
key = {
"user.*"} //设置路由key
)
})
public void receive01(String message){
System.out.println("第一个消费者接收到的message:"+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //不指定队列名相当于是一个临时队列
exchange = @Exchange(value = "logs_topic",type = "topic"), //绑定交换机
key = {
"user.insert.*"} //设置路由key
)
})
public void receive02(String message){
System.out.println("第二个消费者接收到的message:"+message);
}
}
第一个消费者接收到的message:你好,我是 路由topic queue
场景说明:用户注册后,需要发送注册邮件与注册短信,传统的做法有两种,一是串行,而是并行。
使用MQ:引入消息队列后,用户的响应时间 = 写入数据库的时间 + 写入消息队列的时间。
场景说明:双十一狂欢节,用户下单后,订单系统需要调用库存系统,传统的做法是订单系统调用库存系统的接口。这样做有很大的缺陷,假如库存系统故障,会导致用户下单失败,流失订单。
使用MQ:
- 订单系统:用户下单后,订单系统完成持久化操作,将消息写入消息队列,返回用户下单成功;
- 库存系统:订阅下单的消息,获取消息后进行库存操作,就算库存系统故障,消息队列也能够保证消息的可靠传递,不会导致消息丢失。
场景说明:秒杀活动,一般会因为流量过大导致应用挂掉。
使用MQ:用户的请求被服务器收到后,首先写入消息队列,如果加入消息队列数量超过一定的阈值,就直接抛弃该次请求,然后返回给用户一个抢购失败的错误页面。
只是一个主备模式:
- slave节点只存储exchange的信息,队列信息只在master节点,并且master节点宕机后,slave节点也不能反客为主成为新的master节点。
- slave节点同步着master中队列信息,当master启动的时候,消费者可以从slave中拿到队列消息(此刻是slave向master请示获取的)进行消费;但是当master节点宕机后,消费者是不能够访问slave节点进行消费的,必须等到master重新启动才行。
1、将每一个节点下/var/lib/rabbitmq/.erlang.cookie文件进行同步,都设置为一样的
2、给每一个节点都配置域名映射 vim /etc/hosts
172.17.0.4 mq1
172.17.0.5 mq2
172.17.0.6 mq3
3、关闭两个slave节点,执行命令加入集群,启动服务
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@mq1
rabbirmqctl start_app
4、查看集群状态
rabbitmqctl cluster_status