RabbitMQ介绍使用
MQ介绍
-
MQ的优势
应用解耦:提高系统容错率和可维护性
异步提速:提升用户的体验和系统的吞吐量
削峰填谷:提高系统的稳定性
-
MQ的劣势
系统的可用性降低:系统引入的外部依赖越多,稳定性越差。一旦MQ宕机,就会对业务造成影响
系统的复杂度提高:MQ的加入增加的系统的复杂度,以前系统的同步是远程调用,现在是通过MQ异步调用。如何保证消息没有被重复消费?怎么处理消息丢失的情况?如何保证消息传递的顺序性?
一致性问题:假如A系统处理完业务,通过MQ给B、C、D三个系统发送消息数据,如果B、C系统都处理成功,D系统处理失败了。如何保证消息数据处理的一致性?
-
小结
MQ有优势也有劣势,使用MQ需要满足那些条件?
生产者不需要从消费者处得到反馈
容许短暂的不一致性
用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ的成本
-
常见的MQ
RabbitMQ简介
-
RabbitMQ的介绍:RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。它支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
-
RabbitMQ的基础架构
Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这 些属性包括routing-key(路由键)、priority(相对于其他 消息的优先权)、delivery-mode(指出该消息可能需要持 久性存储)等。
Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所 以可以将交换器理解成一个由绑定构成的路由表。
Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列消息一直在队列里面,等待消费者连接到这个队列将其取走。
Connection:网络连接,比如一个TCP连接。
Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务 器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
Broker:表示消息队列服务器实体。
-
RabbitMQ简介
RabbitMQ提供的六种工作模式:简单模式、work queues、Publish/Subscribe发布与订阅模式、Routing路由模式、Topics主题模式、RPC远程调用模式。官网对应模式的介绍:
-
小节
RabbitMQ 是基于AMQP协议使用Erlang语言开发的一款消息队列产品。
RabbitMQ提供 了6种工作模式。
AMQP是协议,类比HTTP。
JMS 是API规范接口,类比JDBC。
RabbitMQ的安装和配置(docker安装)
-
查看仓库里的RabbitMQ
docker search rabbitmq
-
拉取镜像
docker pull rabbitmq:3.6.2-management
-
配置端口 背后运行
docker run -d --name rabbitmq -p 15672:15672 -p 5672:5672 -p 25672:25672 rabbitmq:3.6.2-management
-
安装插件
先执行docker ps 拿到当前的镜像ID
进入容器
安装插件
ctrl+p+q退出当前容器
docker ps docker exec -it 镜像ID /bin/bash rabbitmq-plugins enable rabbitmq_management
-
默认账号密码都是guest
RabbitMQ的使用
RabbitMQ简单模式的使用
-
创建一个新的maven工程,导入依赖
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.18.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency>
-
生产者
public static void main(String[] args) throws IOException, TimeoutException { //第一步 //创建连接工厂类对象 ConnectionFactory factory = new ConnectionFactory(); //设置连接信息 factory.setHost("127.0.0.1");//服务器地址 factory.setPort(5672);//服务器端口号 factory.setUsername("admin"); factory.setPassword("1357"); //设置虚拟主机 factory.setVirtualHost("/"); //第二步 创建连接 Connection conn = factory.newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queueOne"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); /** * 第五步 通过信道向对列发送消息 * 参数说明: * 1. exchange 交换器名称 直接设置队列名称 简单模式 不需要交换器 * 2. routingKey 路由键 指明消息发送到哪个队列 这里直接指明队列名称 * 3. props 发送消息时 附件信息 * 4. body 消息体 */ channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, "olllll".getBytes()); //第六步 释放连接 conn.close(); System.out.println("消息发送完毕!"); }
-
消费者
public static void main(String[] args) throws IOException, TimeoutException { //第一步 //创建连接工厂类对象 ConnectionFactory factory = new ConnectionFactory(); //设置连接信息 factory.setHost("127.0.0.1");//服务器地址 factory.setPort(5672);//服务器端口号 //设置用户名密码默认都是guest factory.setUsername("admin"); factory.setPassword("1357"); //设置虚拟主机(默认"/") factory.setVirtualHost("/"); //第二步 创建连接 Connection conn = factory.newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queueOne"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, false, ((consumerTag, message) -> { System.out.println("consumerTag>>>"+consumerTag); System.out.println("消费消息:"+new String(message.getBody())); //通知mq服务器 消息消费情况 channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 }
RabbitMQ的工作模式
-
模式说明
Work Queues:与入门的简单模式相比,多了一个或一些消费者,多个消费者 共同消费同意队列中的消息
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
-
由于每次发送消息都要创建连接,这里简单封装一下
public class ConnectionUtils { ConnectionFactory factory; private static ConnectionUtils instance; private ConnectionUtils() { //第一步 //创建连接工厂类对象 factory = new ConnectionFactory(); //设置连接信息 factory.setHost("127.0.0.1");//服务器地址 factory.setPort(5672);//服务器端口号 factory.setUsername("admin"); factory.setPassword("1357"); //设置虚拟主机 factory.setVirtualHost("/"); } public static ConnectionUtils getInstance() { if (null == instance) { instance = new ConnectionUtils(); } return instance; } /** * 创建mq连接 * * @return */ public Connection newConnection() { try { return factory.newConnection(); } catch (IOException e) { throw new RuntimeException(e); } catch (TimeoutException e) { throw new RuntimeException(e); } } }
-
生产者
public class Publisher { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "work"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); //任务消息 String []msg={".................","..","..............................................","."}; /** * 第五步 通过信道向对列发送消息 * 参数说明: * 1. exchange 交换器名称 直接设置队列名称 简单模式 不需要交换器 * 2. routingKey 路由键 指明消息发送到哪个队列 这里直接指明队列名称 * 3. props 发送消息时 附件信息 * 4. body 消息体 */ for(String m : msg){ channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, m.getBytes()); System.out.println("发送消息:"+m); } //第六步 释放连接 conn.close(); System.out.println("消息发送完毕!"); } }
-
消费者
public class Consumer { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "work"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); System.out.println(Thread.currentThread().getName()+"等待消费:"); //设置预先读取数量 channel.basicQos(1); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, false, ((consumerTag, message) -> { work(new String(message.getBody())); //通知 mq服务器 活干 过多了 再给我任务 channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 } /** * 执行任务 每个. 代表执行1s * * @param msg */ static void work(String msg) { System.out.println(Thread.currentThread().getName() + "开始执行:" + msg); int len = msg.length(); for (int i = 0; i < len; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(Thread.currentThread().getName() + "结束执行:" + msg); } }
-
小节
在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需有一个节点发送成功即可
Pub/Sub订阅模式
1.模式说明
-
在订阅模型中,多了一个Exchange角色,而且过程略有变化:
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X (交换机)
C:消费者,消息的接收者,会一直等待消息到来
Queue: 消息队列,接收消息、缓存消息
Exchange: 交换机(X) 。一方面,接收生产者发送的消息。另- -方面,知道如何处理消息,例如递交给某个特别队列、
递交给所有队列、或是将消息丢弃。到底如何操作,取英于Exchange的类型。Exchange有 常见以下3种类型:
Fanout: 广播,将消息交给所有绑定到交换机的队列
Fanout: 广播,将消息交给所有绑定到交换机的队列
Topic; 通配符,把消息交给符合routing pattern (路由模式)的队列
Exchange (交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
-
生产者
public class Publisher { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //第四步 声明交换器 //声明交换器名称 String exname = "erp"; channel.exchangeDeclare(exname, BuiltinExchangeType.FANOUT); //交换器类别 fanout 广播 //队列名称 String q1 = "queneOne", q2 = "queueTwo"; /** * //第五步 声明多个队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(q1, true, false, false, null); channel.queueDeclare(q2, true, false, false, null); channel.queueDeclare("queueThree", true, false, false, null); //第六步 绑定交换器与队列 channel.queueBind(q1, exname, ""); channel.queueBind(q2, exname, ""); //第七步 发消息 指定交换器 消息会发送到所有绑定到交换器上的队列 channel.basicPublish(exname, "", MessageProperties.PERSISTENT_TEXT_PLAIN, "重要通知....".getBytes()); //第六步 释放连接 conn.close(); System.out.println("消息发送完毕!"); } }
-
消费者(多个)
public class ConsumerOne { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queneOne"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); System.out.println("消息者One等待消费:"); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, true, ((consumerTag, message) -> { System.out.println("消息者one消费:" + new String(message.getBody())); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 } }
public class ConsumerTwo { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queueTwo"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); System.out.println("消息者two等待消费:"); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, true, ((consumerTag, message) -> { System.out.println("消息者two消费:" + new String(message.getBody())); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 } }
-
Routing路由模式
-
模式说明
队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送在向Exchange发送消息时,也必须指定消息的Routingkey
Exchange不在把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的Routing key完全一直,才会接收到消息
-
生产者
public class Publisher { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //第四步 声明交换器 //声明交换器名称 String exname = "erp-log"; channel.exchangeDeclare(exname, BuiltinExchangeType.DIRECT); //交换器类别 direct 路由键 //队列名称 String q1 = "queneOne", q2 = "queueTwo"; /** * //第五步 声明多个队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(q1, true, false, false, null); channel.queueDeclare(q2, true, false, false, null); //第六步 绑定交换器与队列 同时指明 路由键 channel.queueBind(q1, exname, "info"); channel.queueBind(q1, exname, "warning"); channel.queueBind(q2, exname, "error"); //第七步 发消息 指定交换器 消息会发送到所有绑定到交换器上的队列 channel.basicPublish(exname, "info", MessageProperties.PERSISTENT_TEXT_PLAIN, "用户登录成功".getBytes()); channel.basicPublish(exname, "warning", MessageProperties.PERSISTENT_TEXT_PLAIN, "用户密码是明文,不安全....".getBytes()); channel.basicPublish(exname, "error", MessageProperties.PERSISTENT_TEXT_PLAIN, "UserSerivce NullPointerExction ....".getBytes()); // 释放连接 conn.close(); System.out.println("消息发送完毕!"); } }
-
消费者(多个)
public class ConsumerOne { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queneOne"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); System.out.println("消息者One等待消费:"); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, true, ((consumerTag, message) -> { System.out.println("消息者one消费:" + new String(message.getBody())); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 } }
public class ConsumerTwo { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queueTwo"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); System.out.println("消息者two等待消费:"); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, true, ((consumerTag, message) -> { System.out.println("消息者two消费:" + new String(message.getBody())); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 } }
public class ConsumerThree { public static void main(String[] args) throws IOException, TimeoutException { //第二步 创建连接 Connection conn = ConnectionUtils.getInstance().newConnection(); //第三步 创建信道 Channel channel = conn.createChannel(); //设置队列名称 String queueName = "queueThree"; /** * //第四步 声明队列 * 参数说明: * 1. queue 队列的名称 * 2. durable 队列是否持久化 如果持久化 mq服务器重启后 仍然存在 否则删除 * 3. excluesive 是否独占连接 如果设置为true,生产者和消费者必须是同一个连接 * 4. autodelete 是否自动删除队列 如果durable为true,队列持久化 不会删除 为false 当对列没有消息 会自动删除 * 5. arguments properties属性的参数 附件在队列上的 */ channel.queueDeclare(queueName, true, false, false, null); System.out.println("消息者two等待消费:"); //第五步 消费消息 //autoAck 为true 自动应答 channel.basicConsume(queueName, true, ((consumerTag, message) -> { System.out.println("消息者two消费:" + new String(message.getBody())); }), consumerTag -> { }); //消息者不要关闭连接 队列中的消息 消费完 处于阻塞状态 一旦有消息 接着消费 } }
-
Topics通配符模式
-
模式说明
Topic主题模式可以实现Pub/Sub发布订阅模式和Routing路由模式的功能,只是Topic在配置routing key的时候可以使用通配符,显得更加灵活。
*对应一个单词,#对应一到多个单词
工作模式总结
-
简单模式HelloWorld
个生产者、-个消费者,不需要设置交换机(使用默认的交换机)。
-
工作队列模式 Work Queue
个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
-
发布订阅模式Publish/subscribe
需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
-
路由模式Routing
需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列。
-
通配符模式Topic 需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列。