RabiitMQ入门基础学习一
MQ 简介
MQ全称 Message Queue(*消息队列*),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信
分布式系统通信两种方式:直接远程调用 和 借助第三方 完成间接通信
消息发送方称为生产者,接收方称为消息消费者
优劣势:
-
⚫系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。
一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用? -
⚫系统复杂度提高
MQ 的加入增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。
如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性? -
⚫一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。如何保证消息数据处理的一致性? -
注意:
生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
容许短暂的不一致性。
RabbitMQ 简介
首先介绍AMQP协议,即 Advanced Message Queuing Protocol(高级消息队列协议)是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。
基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。
RabbitMQ 采用 Erlang 语言开发。Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。
RabbitMQ 基础架构如下图:
RabbitMQ 工作模式
- Hello World 模式
生产者将消息放入队列,消费者监听该队列,若有消息则消费。 - Work queues
竞争模式,多个消费者监听一个队列,谁先拿到消息,谁先消费。
类比于抢红包。 - Publish/Subscribe(BuiltinExchangeType.FANOUT)
广播模式,生产者将消息发送到交换机,交换机一次性将消息发送到所绑定的所有队列上,而消费者监听的队列一旦有消息,即可消费。
类比于群发邮件。/* 此时交换机的队列绑定 queueBind(String queue, String exchange, String routingKey) 参数: 1. queue:队列名称 2. exchange:交换机名称 3. routingKey:路由键,绑定规则 如果交换机的类型为fanout ,routingKey设置为"" */ channel.queueBind(queueName1, exchangeName, "");
- Routing
生产者将消息发送到交换机,交换机根据指定的routeKey分发到指定的队列,而消费者监听的队列收到消息时,即可消费。//路由route绑定队列queue /* queueBind(String queue, String exchange, String routingKey) 参数: 3. routingKey:路由键,绑定规则 如果交换机的类型为routing ,routingKey设置为指定特殊值 案例:consumer1 只消费routingKey为error的日志消息; consumer2 消费routingKey为error、info、warning的日志消息; */ channel.queueBind(queueName1, exchangeName, "error"); channel.queueBind(queueName2, exchangeName, "error"); channel.queueBind(queueName2, exchangeName, "info"); channel.queueBind(queueName2, exchangeName, "warning");
- Topic
生产者将消息发送到交换机,交换机根据指定的routeKey分发到指定的队列,此时的routeKey是pattern类型(*
代表一个;#代表0个或多个),而消费者监听的队列收到消息时,即可消费。//路由route绑定队列queue /* queueBind(String queue, String exchange, String routingKey) 参数: 3. routingKey:路由键,绑定规则 如果交换机的类型为routing ,routingKey设置为指定特殊值 案例:consumer1 只消费routingKey为.error的日志消息和order相关的日志消息; 匹配规则为: #.error || order.* # 代表0个或任意个字符,* 代表一个字符 consumer2 消费routingKey为任意的日志消息; 匹配规则为: #.* */ channel.queueBind(queueName1, exchangeName, "#.error"); channel.queueBind(queueName1, exchangeName, "order.*"); channel.queueBind(queueName2, exchangeName, "#.*");
RabbitMQ 高级特性
-
消息可靠投递(生产端的可靠性保证)
(1)从生产端->exchange/** * 设置ConfirmCallback回调函数后: * 当消息发送到exchange后调用confirm方法。 * 在方法中判断ack, 为true则发送成功,否则发送失败。 */ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { /** * @param correlationData 相关配置 * @param ack 消息发送是否成功 * @param cause 失败原因 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.out.println("confirm 执行了"); if (ack) System.out.println("消息发送到exchange成功:"+ cause); else System.out.println("消息发送到exchange失败!!!:"+ cause); } });
(2)从exchange -> queue
/** * 设置ReturnCallback回调函数后: * 当消息从exchange发送到queue成功时,不会回调。 * 当消息从exchange发送到queue失败时: * 设置exchange处理消息模式: * 1、如果消息没有从exchange到queue时,则丢弃消息(默认) * 2、如果消息没有从exchange到queue时,则返回消息发送方,执行回调(需设置 rabbitTemplate.setMandatory(true)) */ rabbitTemplate.setMandatory(true); rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { /** * * @param message 消息体 * @param replyCode 错误码 * @param replyText 错误信息 * @param exchange 交换机 * @param routingKey 路由键 */ @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println("return 方法执行了~~~"); System.out.println(message); System.out.println(replyCode); System.out.println(replyText); System.out.println(exchange); System.out.println(routingKey); } });
-
Consumer Ask(消费端的可靠性保证)
(1)直接消费消息,不管之后的业务逻辑是否出现异常,消息消费后就直接丢弃
(2)手动确认消息,若业务逻辑正常执行,则手动确认消息channel.basicAck()
/** * void basicAck(long deliveryTag, boolean multiple) throws IOException; * deliveryTag :表示当前消息的标签 * multiple : 允许是否同时消费多条消息 */ channel.basicAck(deliveryTag, true);
(3)手动拒绝消息,若业务逻辑出现异常,则手动拒绝消息
channel.basicNack()
/** * void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException; * requeue:是否重新回到队列。 * 若为true,则消息重新回到queue, broker会将消息重新发送到消费端 */ channel.basicNack(deliveryTag, true, true);
-
消费端限流
(1)确保ack机制为手动确认 (2)在<rabbit:listener-container>配置prefetch=1(数字代表一次拉取一条)属性
-
TTL(time to live)
队列过期时间: 一旦队列过期时间到了,那么队列中所有的消息都会被丢弃 消息单独过期时间: 如果队列和消息都设置了过期时间,则以时间短的为准; 队列过期后,则会将所有消息丢弃; 消息过期后,只有在队列顶端的消息才会去判断是否过期,若在队列中间是不会判断是否过期,只有当中间的消息达到队列顶端时,才会去判断是否过期,过期则丢弃。
-
死信队列
死信队列,Dead Letter Exchange(DLX,死信交换机),当消息成为dead message 后,可以被重新发送到另一个交换机B,此时这个交换机B则为DLX。
消息如何才会成为死信:(1)消息的数量超过了队列消息长度 (2)消息自身过期或者队列过期 (3)被手动拒绝 basicNask()/basicReject() 且不回到队列的消息 requeue=false
<!-- 1 声明正常交换机、队列 --> <rabbit:queue name="order_queue" id="order_queue" auto-declare="true"> <!-- 3 绑定死信交换机、routeKey等 --> <rabbit:queue-arguments> <entry key="x-dead-letter-exchange" value="dlx_order_exchange"/> <entry key="x-dead-letter-routing-key" value="dlx.order.cancel"/> <!-- 4.1 设置过期时间 --> <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/> <!-- 4.2 设置队列最大长度 --> <entry key="x-max-length" value="5" value-type="java.lang.Integer"/> </rabbit:queue-arguments> </rabbit:queue> <rabbit:topic-exchange name="order_exchange" id="order_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding pattern="order.#" queue="order_queue"></rabbit:binding> </rabbit:bindings> </rabbit:topic-exchange> <!-- 2 声明死信交换机、队列 --> <rabbit:queue name="dlx_order_queue" id="dlx_order_queue" auto-declare="true"/> <rabbit:topic-exchange name="dlx_order_exchange" id="dlx_order_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding pattern="dlx.order.#" queue="dlx_order_queue"></rabbit:binding> </rabbit:bindings> </rabbit:topic-exchange>
-
延迟队列
延迟队列不会立即被消费,只有到达指定时间后,才会被消费。 延迟队列在rabbitMq中是没有实现的,可以通过 `TTL+死信队列` 实现。
案例:用户下一个订单,30min后未支付则取消订单,库存回滚。下图为
TTL+死信队列
实现得延迟队列。
-
日志监控
-
消息追踪
在使用任何消息中间件的过程中,难免会出现某条消息异常丢失的情况。 对于RabbitMQ而言,可能是因为生产者或消费者与RabbitMQ断开了连接,而它们与RabbitMQ又采用了不同的确认机制;也有可能是因为交换器与队列之间不同的转发策略; 甚至是交换器并没有与任何队列进行绑定,生产者又不感知或者没有采取相应的措施;另外RabbitMQ本身的集群策略也可能导致消息的丢失。 这个时候就需要有一个较好的机制跟踪记录消息的投递过程,以此协助开发和运维人员进行问题的定位。 在rabbitmq中使用Firehose和rabbitmq-tracing插件来追踪消息。 firehose的机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式发送到默认的exchange上。 这个默认的exchange的名称为amq.rabbitmq.trace,它是一个topic类型的exchange。 Firehose: rabbitmqctl trace_on: 开启Firehose命令 rabbitmqctl trace_off: 关闭Firehose命令
-
消息可靠性保障(消息补偿)
消息可靠性主要体现在两个方面:producer发送消息失败;consumer消费消息失败。上图分为4个流程: 1、producer操作数据库并发送消息到Q1,被consumer消费并写入DB。(步骤1、2、4) 2、consumer成功消费后发送确认消息到Q2,并将消息写入数据库MDB。(步骤5-7) 3、producer延迟发送同一条消息到Q3,在回调检查服务中和MDB数据库中得消息id进行对比, 若存在则代表消息消费成功,若不存在则代表消息消费失败,执行步骤8,调用producer重新发送消息。(步骤3、8) 4、若producer发送消息失败。 通过定时检查服务(可定时为1h),对比DB和MDB两个数据库中得消息是否一致,若DB比MDB中得消息多, 则代表存在消息发送失败,则执行步骤9,调用producer重新发送消息。(步骤9)
-
消息幂等性保障(乐观锁解决方案)
幂等性指:一次和多次请求一个资源,对于资源本身应具有同样得结果。也就是说,其任意多次执行对资源本身所产生得影响均与一次执行得影响相同。 在MQ中指,消费多条相同的消息,得到与消费该消息一次相同得结果。
在此介绍一种通过数据库乐观锁得机制来应对,即通过数据中得version字段来处理多次消费相同的消息情况。即第一次消费消息时,version=1,执行sql
version=version+1
后,此时的version=2,在执行sql where id=1 and version=1
时抛出异常,不再进行操作。
RabbitMQ 集群搭建
- 镜像队列
- HA环境搭建