目前项目中需要使用消息队列,同时行内开始搞国产化,综合几种消息队列的产品决定引入RabbitMQ,一起来学习下这款消息队列中间件吧。
RabbitMQ
AMQP协议模型
- AMQP协议(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议.
- 流程:(一般流程)
- 生产者生产消息–》与Server建立连接–》找到对应的Virtual Host–》到对应的交换机–》然后将消息放入到对应 的queue
- 消费者–》连接Server–》找到对应的Virtual Host–》到对应的queue中取到消息
- 官方提供7种消息的发布的模式
几种消息队列产品的对比:
- Activemq: 性能受到诟病。成熟,完善的手册。
- kafka: 基于大数据的消息中间件,追求高的吞吐量,开始的目的是用于日志收集与传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格的要求。适合产生大数据的互联网服务数据收集业务。
- RocketMQ: 官方版本支持事务,开源的版本不支持事务
- Rabbitmq:erlang语开发的开源消息队列系统,基于AMQP协议来实现。AMQP主要特征面向消息、队列、路由(包括点对点和发布订阅)、可靠性、安全。AMQP协议更多用于企业系统对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
- 虚拟主机(类比数据库中的表的概念)与用户进行绑定(首先创建用户,然后创建虚拟主机,再将虚拟主机和用户进行绑定)。
rabbitmq官方提供其中消息的方式
源码可以到:源码GitHub地址
方式一:直连:简单方式
- 生产者代码
public class Provider {
// 直连方式:生产消息
@Test
public void sendMesssge() throws IOException, TimeoutException {
/*//创建连接mq的工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接mq的主机
connectionFactory.setHost("localhost");
// 设置端口 5672
connectionFactory.setPort(5672);
//设置连接的虚拟主机
connectionFactory.setVirtualHost("/rabbitmqtest1");
//设置用户名和密码
connectionFactory.setUsername("rabbitmqtest1user");
connectionFactory.setPassword("rabbitmqtest1user");
//获取连接对象
Connection connection = connectionFactory.newConnection();*/
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道对象
Channel channel = connection.createChannel();
//通道绑定对应的消息队列
// 参数1:队列名称(如果首次会自动新建名称)
// 参数2:durable是否持久化 false 不持久话. true 持久化
// 参数3: exclusive 是否独占队列 false不独占 ,true 独占队列
// 参数4: autoDelete 消息消费完后是否删除 true 删除 ,false 不删除
// 参数5: 其他参数
channel.queueDeclare("rabbitmqtestqueue1", false, false, false, null);
//发布消息
// 参数1:交换机(简单模式没有交换机) 参数2: 队列名称 参数3:传递消息额外设置 参数4:消息的具体内容
channel.basicPublish("", "rabbitmqtestqueue1", null, "你好".getBytes());
/*channel.close();
connection.close();*/
RabbitMQConnectUtils.closeChannelAndConnection(channel,connection);
}
}
- 消费者代码
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接工厂对象
/* ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/rabbitmqtest1");
factory.setUsername("rabbitmqtest1user");
factory.setPassword("rabbitmqtest1u);
Connection connection = factory.newConnection();*/
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("rabbitmqtestqueue1", false, false, false, null);
//消费消息
// 参数1:消费哪个队列的消息(队列名称)
// 参数2:开启消息的自动确认机制
// 参数3:消费时的回调
channel.basicConsume("rabbitmqtestqueue1", 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));
}
});
// consumer端不建议关闭连接和通道(否则可能会接受不到回调,就结束了.)
// channel.close();
// connection.close();
}
}
总结:
消费者和生产者的参数配置必须严格一致,否则可能出现异常问题。
通讯都是基于通道(Channel)的,生产者通过通道往队列里放消息;消费者通过通道从队列里取消息。
方式二:工作队列模型
- 生产者代码
public class Provider {
@Test
public void sendMessage() throws IOException {
//获取连接对象
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道
Channel channel = connection.createChannel();
//通过通道声明队列
channel.queueDeclare("workqueue", true, false, false, null);
for (int i = 0; i < 20; i++) {
//生产消息
channel.basicPublish("", "workqueue", MessageProperties.PERSISTENT_TEXT_PLAIN, (i + "< > 你好工作队列").getBytes());
}
RabbitMQConnectUtils.closeChannelAndConnection(channel, connection);
}
}
- 消费者1代码
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("workqueue", true, false, false, null);
channel.basicConsume("workqueue", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
try {
Thread.sleep(1000); //由于工作队列默认的是对每个消费则平均分配任务,模拟其中一个处理较慢,是否还是平均分配
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Consumer1 获取到工作消息--> " + new String(body));
}
});
}
}
- 消费者2代码
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("workqueue", true, false, false, null);
channel.basicConsume("workqueue", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("Consumer1 获取到工作消息--> "+new String(body));
}
});
}
}
总结:平均分配是会产生处理慢的依旧会积累。
By default, RabbitMQ will send each message to the next consumer, in sequence. On average every consumer will get the same number of messages. This way of distributing messages is called round-robin. Try this out with three or more workers.
默认情况下,RabbitMQ会将每条消息依次发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环。让三个或更多的工人试试这个方法。
弊端:
1.当一个消费者的处理能力很差,则会导致消息不能被及时的处理掉,造成消息的堆积
2.(自动确认机制中)如果其中一个消费者宕机,则会导致分配给它的任务丢失。
消息确认机制:
// 参数:参数2:自动确认 true时, 消费者自动向rabbitmq确认消息消费. false 不会自动确认
channel.basicConsume("workqueue", true, new DefaultConsumer(channel) {}
// 此时消息队列会将消息一次性平均分给不同的消费者(弊端1);自动确认(弊端2)
- 处理办法
// 1.首先通道告诉队列一次只消费一个消息
channel.basicQos(1);//每次只能消费一个消息
// 参数:参数2:自动确认 true时, 消费者自动向rabbitmq确认消息消费. false 不会自动确认
// 2.关闭自动确认机制
channel.basicConsume("workqueue", false, new DefaultConsumer(channel) {}
// 业务逻辑处理
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
//3.手动确认 参数1:手动确认消息标识 ,参数2:false,每次确认一个
channel.basicAck(envelope.getDeliveryTag(),false);
- 处理问题的代码:
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);//每次只能消费一个消息
//通道与队列建立连接
channel.queueDeclare("workqueue", true, false, false, null);
// 参数:参数2:自动确认 true时, 消费者自动向rabbitmq确认消息消费. false 不会自动确认
channel.basicConsume("workqueue", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
try {
Thread.sleep(1000); //由于工作队列默认的是对每个消费则平均分配任务,模拟其中一个处理较慢,是否还是平均分配
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Consumer1 获取到工作消息--> " + new String(body));
//手动确认 参数1:手动确认消息标识(确认队列中哪个具体的消息) ,参数2:false,每次确认一个
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
总结:
上述代码的优点
1:避免一次平均分配导致处理慢的消费者卡住,实现能者多劳。提高系统吞吐量
2:如果未确认,则在会一直在队列里。
方式三:(fanout)扇出、广播
应用场景:发一条消息被不同的业务系统同时去执行。(购物车结算时:同时调用订单系统、库存系统。)
-
消费者1和消费者2,两个拿到的消息是同一个消息只是做不同的处理。
-
生产者代码
public class Provider {
public static void main(String[] args) throws IOException {
//获取连接对象
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道
Channel channel = connection.createChannel();
// 参数: 参数1: 交换机的名称, 参数2 : 交换机的类型: fanout: 广播类型
channel.exchangeDeclare("logs", "fanout");
//发送消息
channel.basicPublish("logs","", MessageProperties.PERSISTENT_TEXT_PLAIN,"fanout Message".getBytes());
RabbitMQConnectUtils.closeChannelAndConnection(channel,connection);
}
}
- 消费者代码(消费者代码相同,只是最后的业务处理逻辑不同)
- 特点:所有的消费者拿到的消息都是相同的,处理逻辑不同。(发一条消息被不同的业务系统同时去执行)
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 创建连接
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//创建通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs", "fanout");
//创建临时队列
String queueName = channel.queueDeclare().getQueue();
//将临时队列和交换机建立连接
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("consumer1获取到了消息: " + new String(body));
}
});
}
}
应用场景:
银行核心发送消息到每一个业务系统,不同的业务系统拿到相同的信息做不同的处理。
购物车模块结算时,需要同时掉订单、库存、支付等模块
方式四:路由模型
- 生产者代码:
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道
Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
//绑定交换机
channel.exchangeDeclare(exchangeName, "direct");
//路由模式:需要routingKey
String routingKey = "error";
// 参数1:交换机名 参数2:路由key 参数3:持久化消息. 参数4:真正要发送的消息
channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ("这是路由模式下的rabbitmq消息[" + routingKey + "]请接收").getBytes());
//关闭资源
RabbitMQConnectUtils.closeChannelAndConnection(channel, connection);
}
}
- 消费者1代码:
public class Consumer1 {
public static void main(String[] args) throws IOException {
//创建连接
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道
Channel channel = connection.createChannel();
//通道绑定队列
channel.exchangeDeclare("logs_direct", "direct");
// 获取临时queue
String queue = channel.queueDeclare().getQueue();
//将队列和交换机绑定,同时声明接受那些routing key
channel.queueBind(queue, "logs_direct", "info");
channel.queueBind(queue, "logs_direct", "error");
channel.queueBind(queue, "logs_direct", "warning");
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("Consumer1 获取消息--> " + new String(body));
}
});
}
}
总结:
模式4:基于不同的应用场景对消息进行不同的执行路线,指定特定的程序去处理不同的业务。获取有选择的接收可能。
弊端:但是还不够灵活,不能基于多个标准进行路由。
方式五:Topics
topics类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topics类型Exchange可以让队列绑定Routing key 的时候使用通配符。这种模式的routingkey 一般是由一个或多个单词组成,多个单词之间以”.“ 分割。如:account.name
通配符:
*(star) can substitute for exactly one word. 仅匹配一个词
#(hash) can substitute for zero or more words. 匹配0个或多个
例如:
account.# 可以匹配: account 或account.name.value或account.name 等
account.* 可以匹配: account.name 或account.value
- 消费者代码:
public class Provider {
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道Channel
Channel channel = connection.createChannel();
//交换机和通道绑定
String exchangeName="logs_topic";
channel.exchangeDeclare(exchangeName, "topic");
//发布消息
String routingKey = "user.save.aaa";//路由key
channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ("模型5基于动态路由的方式发布消息RoutingKey-> " + routingKey + "<").getBytes());
RabbitMQConnectUtils.closeChannelAndConnection(channel, connection);
}
}
- 消费者1代码:仅支持
"user.*"
user后面跟一个词的情况
public class Consumer1 {
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道
Channel channel = connection.createChannel();
//通道绑定交换机
String exchangeName = "logs_topic";
channel.exchangeDeclare(exchangeName, "topic");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
//队列绑定交换机
String routingKey = "user.*";
channel.queueBind(queue, exchangeName, routingKey);
//消费消息
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到了消息--> " + new String(body));
}
});
}
}
- 消费者2代码:支持
"user.#"
user后面跟0个或多个词的情况
public class Consumer2 {
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
//获取通道
Channel channel = connection.createChannel();
//通道绑定交换机
String exchangeName = "logs_topic";
channel.exchangeDeclare(exchangeName, "topic");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
//队列绑定交换机
String routingKey = "user.#";
channel.queueBind(queue, exchangeName, routingKey);
//消费消息
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer2 接收到了消息--> " + new String(body));
}
});
}
}
总结:
topics 模式和模式4 路由模式的区别在于topic支持动态路由,即:路由key可以使用通配符。
方式六:RPC
方式七: Publisher Confirms
总结
区别:
- 1.是否使用到交换机。
- a:如果不使用交换机,生产者和消费者直接通过队列进行数据发布与获取(当然需要在Channel的帮助下)。
- b:如果使用交换机:则生产者和消费者则是通过Exchange交换机进行数据的交换。
- 2:不使用交换机的情况:
- a:模式1:一对一,生产者和消费者一一对应。
- b:模式2:一对多,一个生产者对应多个消费者,消息被平均分给每一个消费者。(需要关闭自动消息确认机制,提高服务器的吞吐量)
- 3:使用交换机的情况 :
- a: 模式3:扇出模型:一个消息可以被多个消费者消费。
- b: 模式4:路由模型:一个消息可以指定被某一个消费者消费
- c: 模式5:topics动态路由模型:在模式4的基础上可以使用动态路由key
生产者代码编写:
//获取连接
//获取通道Channel
//交换机和通道绑定
//发布消息
消费者代码编写:
//获取连接
//获取通道
//通道绑定交换机
//获取临时队列
//队列绑定交换机
//消费消息