1. Exchange :接收消息,并且根据路由键转发消息所绑定的队列
蓝色:生产A,B将消息投递到Exchange上 ,Exchange根据RoutingKey转发到指定的队列中
绿色:消费者1,2,3监听队列来获取消息
黄色:根据RoutingKey绑定交换机和队列
红色:rabbitMQ服务
2. 交换机属性
Name:交换机名称
Type:交换机类型 direct,topic,fanout,headers
Durability:是否需要持久化,true 为持久化
Auto Delete:当最后一个绑定到Exchange上的队列删除后,自动删除该Exchange
Internal:当前Exchange是否用于RabbitMQ内部使用,默认为false(一般不去做修改)
Arguments:扩展参数,用于扩展AMQP协议自制定化使用
所有发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue,不允许模糊匹配路由键,
投递消息中的routingKey 要和队列中binding的key完全一致
Direct模式可以使用RabbitMQ中自带的Exchange:default Exchange,所以不需要将Exchange进行任何绑定,消息传递时,RouteKey必须完全匹配才会被队列接收,否则该消息将会被抛弃
routing key中的key要和队列名完全一致,就会将消息直接路由到队列中,否则抛弃
当routing key和队列名完全一致时,不需要binding,可以直接投递
所有发送到 Topic Exchange 的消息被转发到所有关心RouteKey中指定的Topic的Queue上
Exchange将 RouteKey 和某个Topic进行模糊匹配,此时队列需要绑定一个Topic
模糊匹配可以使用通配符
符号
" # " 匹配一个到多个词 ,“log.#” 能够匹配到"log.info.ao"
" * " 匹配不多不少一个词 , “log.*” 只会匹配到 "log.error"
队列中的routing key会和消息中的 routing key进行匹配,将消息投递到所有符合匹配规则的队列中
将消息路由给绑定到它身上的所有队列。不同于直连交换机,路由键在此类型上不启任务作用。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的发送给这所有的N个队列
转发消息的速度最快,不需要去做匹配路由规则,性能最好
头部交换机和扇形交换机都不需要路由键 routing Key,交换机时通过Headers头部来将消息映射到队列的,有点像HTTP的Headers,Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串(string)而是Object类型。
消费方指定的headers中必须包含一个"x-match"的键。键"x-match"的值有2个:all和any。
Exchange 和 Exchange 、Queue之间的连接关系
Binding 中 可以包含Routing Key或者参数
消息队列,实际存储消息数据
队列属性
**Name**:交换机名称
**Durability**:是否需要持久化,Durable 为持久化,Transient 为不持久化
**Auto Delete**:如果为yes,当最后一个监听被移除后,该队列也会被自动删除
**arguments(其他属性):**
**1、x-message-ttl:** 消息的过期时间,单位:毫秒
**2、x-expires:** 队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒
**3、x-max-length:** 队列最大长度,超过该最大值,则将从队列头部开始删除消息
**4、x-max-length-bytes:** 队列消息内容占用最大空间,受限于内存大小,超过该值则从队列头部开始删除消息
**5、x-overflow:** 设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx
**6、x-dead-letter-exchange:** 死信交换器名称,过期或被删除(因队列长度超长或因空间超出该值)的消息可指定发送到该交换器中
**7、x-dead-letter-routing-key:** 死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值
**8、x-single-active-consumer:** 表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
**9、x-max-priority:** 队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级
**10、Lazy mode:** 将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息
**11、x-queue-master-locator:** 在集群模式下设置镜像队列的主节点信息
服务器和应用程序之间传送的数据
消息的组成:Properties , Payload(Body)
消息的属性
**delivery mode**: 是否持久化
**headers** :可以自定义属性
**content_type** :类型
**content_encoding** :字符集
**priority** :优先级 0-9 优先级越来越高
**correlation_id** :消息唯一id
**reply_to** :消息指定返回队列
**expiration** :消息过期时间,如果到期没有被消费就会被MQ删除
**message_id** :消息id
虚拟地址,最上层的消息路由,用于进行逻辑隔离
一个Virtual Host里面有若干个 Exchange 和 Queue
同一个Virtual Host里面不能有相同名称的 Exchange 和 Queue
什么是生产端的可靠性投递?
1.保障消息的成功发出
2.保障MQ节点的成功接收
3.发送端收到节点MQ(Broker)的确认应答
4.完善的消息进行补偿机制
BAT大厂解决方案:
(1) 、消息落库,对消息状态进行打标
蓝色:生产者,将消息投递到MQ
Step1:将业务数据入库(BIZ),并将发送的消息入库(MSG)
Step2:发送消息到Broker
Step3: Broker 确认收到 返回应答给生产者( Confrim Listener 异步监听相应)
Step4: 将消息入库的记录状态更新为投递成功状态
Step5:定时拉取状态为0(发送失败消息)
Step6: 生产者重新发送,并将入库的消息 count + 1
Step7: 重试发送3次的消息将状态更新为无法发送
优点:操作简单,易使用
缺点:对数据库进行多次insert和update操作,在高并发的业务场景下性能有瓶颈
适用场景:并发小的应用
(2) 、消息的延迟投递,做二次确认,回调检查
Upstream:上游服务(生产者),Downstream: 下游服务(消费者),MQ Broker(MQ集群)
Callback : 回调服务
Step1: 业务数据入库结束后做第一次消息的发送
Step2: 发送一条延迟消息的检查(定5分钟)
Step3: 消费者监听队列消费消息
Step4: 消费者回送相应消息到MQ(重新生成一条消息投递到MQ)
Step5: Callback 服务监听消费者投递回送消息的队列,如果收到回送消息则将消息入库
Step6: Callback 服务监听生产者投递的延时检查消息,检查数据库中是否有下游服务的回送消息,如果有回送消息的记录则结束。没有的话进行补偿机制,Callback服务主动发起RPC 通信(参数是业务的id或者别的唯一标识),通知上游服务消息投递失败,上游服务查询业务数据库获取数据重新发送消息
优点 : 节省数据库操作,DB解耦,提高并发性能
缺点 : 业务需要加外围监听服务
适用场景: 对高并发要求高的应用
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用
例如乐观锁:update Order set COUNT= COUNT - 1 , VERSION = VERSION +1 where ORDER_ID =1 AND VERSION =1
高并发下可能COUNT 只有1个 ,但是同时又两个请求,如果同时处理COUNT会成为负数业务不允许
解决办法:添加乐观锁 每次操作先查询version号,执行操作将version号带上,执行成功后将version +1 ,如果同时两个请求也第一个先将version更新成2完成了操作,第二个请求则会失败
消费端-幂等性保障
在海量订单产生的业务高峰期,如何避免消息的重复消费问题?
消费端实现幂等性,就意味,我们的消息永远不会消费多次,即时我们收到了多条一样的消息
解决方案:
1、唯一ID + 指纹码 机制 ,利用数据库主键去重
唯一ID + 指纹码(业务规则拼接而成) ,利用数据库主键去重
SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码
优点:实现方法简单
缺点:高并发下数据库写入的性能瓶颈
解决方法:跟进 ID 进行分表分库 ,实现分压分流来提高性能
2、 利用redis的原子性去重
通过redis 实现
业务id来区分是否唯一,通过EXISTS来判断是否存在 ,存在则忽略,不存在则set
使用redis的问题:
第一:是否要对数据落库,如果落库的话怎么解决数据库和缓存间如何做到原子性
第二:如果不进行落库,都存储到缓存中,如何设置定时同步的策略(同步到数据库)
什么是Confirm消息确认机制:
消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给生产者一个应答
生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,该方式是可靠性投递的核心保障
send message:发送消息 broker confirm:回送相应 Confirm Listener:异步监听Broker的响应
springboot 如何实现Confirm 确认消息?
#rabbitmq 消息发送到交换机确认机制,ack
spring.rabbitmq.publisher-confirms=true
@Component
@Slf4j
public class RabbitSender {
@Autowired
private RabbitTemplate rabbitTemplate;
private final ConfirmCallback confirmCallback = (correlationData, ack, s) -> {
log.info("correlationData={}",correlationData); //消息唯一id
log.info("ack={}",ack);
if (ack){
log.info("更新订单状态");
}else {
log.error("投递失败处理,重发机制");
}
};
public void sendOrder(OrderMessageDto message ) throws Exception{
rabbitTemplate.setConfirmCallback(confirmCallback);
log.info("业务数据入库");
CorrelationData correlationData = new CorrelationData("test-hello"+ LocalDateTime.now().toString());
rabbitTemplate.convertAndSend("order-exchange","order.test",message,correlationData);
}
}
Return 消息机制:
Return Listener 用于处理一些不可路由的消息
生产者通过指定一个Exchange 和 RoutingKey,把消息投递到某个队列中,然后消费者监听队列进行消息的消费操作
但在某些情况下,如果我们发送消息的时候,当前的Exchange不存在或者指定的RoutingKey路由不到,Return Listener 来监听这种不可达的消息
#rabbitmq 是否确认回调
spring.rabbitmq.publisher-returns=true
# true 监听器接收不可达消息进行处理,false broker自动删除消息 默认为false
spring.rabbitmq.template.mandatory=true
@Component
@Slf4j
public class RabbitSender {
@Autowired
private RabbitTemplate rabbitTemplate;
private final RabbitTemplate.ReturnCallback callback = (message, errorCode, text, exchange, routingKey)
-> {
log.error("return exchange :{},routingKey:{},message:{},code:{},text:{}"
,exchange,routingKey,message,errorCode,text);
log.info("找不到对应的队列");
};
public void sendOrder(OrderMessageDto message ) throws Exception{
rabbitTemplate.setReturnCallback(callback);
CorrelationData correlationData = new CorrelationData("test-hello"+ LocalDateTime.now().toString());
rabbitTemplate.convertAndSend("order-exchange","order.test",message,correlationData);
}
}
消费端的手工ACK和NACK
ACK 消费成功,
NACK 消费失败 ,将消息重新投递回broker中队列的尾端( 重回队列)
public void onOrderMessage(@Payload OrderMessageDto messageDto, @Headers Map<String,Object> headers, Channel channel) throws Exception{
log.info("message : {}",messageDto.toString());
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
if(messageDto.getOrderNumber().equals("")){
/**
* multiple 是否批量处理
* requeue 是否重回队列
* 将消息重新投递回broker中队列的尾端( 重回队列)
*/
channel.basicNack(deliveryTag,false,true);
}else{
//手工ack 确认签收
channel.basicAck(deliveryTag,false);
}
消费端进行消费的时候,由于业务异常我们可以进行日志记录,然后进行补偿。
如果由于服务器宕机等严重问题,那我们就需要手工ack保障消费端消费成功。
消费端重回队列
消费端重回队列是为了对没有成功消费的消息,把消息重新投递会broker。
一般实际开发都会关闭重回队列,设置为false
场景:在rabbitmq中有上万条数据未被消费,我们启动消费端后会有巨量的消息推送过来,但是我们单个客户端无法同时处理这么多消息,导致服务器奔溃
RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数量的消息(通过基于consume 或者 channel设置Qos的值)未被确认前,不进行消费新的消息
TTL(time to live):生存时间
RabbitMQ支持消息过期时间,在发送消息时指定消息的过期时间。
RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息就会自动清除
1、添加队列过期时间
2、 查看队列
可以看到test3-queue队列的消息过期时间为10000毫秒,队列的过期时间为20000毫秒
springboot下配置队列
spring.rabbitmq.listener.order.queue.name=test3-queue
spring.rabbitmq.listener.order.queue.durable=true
spring.rabbitmq.listener.order.exchange.name=test3-exchange
spring.rabbitmq.listener.order.exchange.type=topic
spring.rabbitmq.listener.order.exchange.durable=true
spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions=true
spring.rabbitmq.listener.order.key=test3.*
@RabbitHandler
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(
value = "${spring.rabbitmq.listener.order.queue.name}",
durable = "${spring.rabbitmq.listener.order.queue.durable}",
arguments = {
@Argument(name="x-expires",value = "20000",type = "java.lang.Integer"),
@Argument(name="x-message-ttl",value = "10000",type = "java.lang.Integer")
}
),
exchange = @Exchange(
value = "${spring.rabbitmq.listener.order.exchange.name}" ,
type = "${spring.rabbitmq.listener.order.exchange.type}" ,
durable = "${spring.rabbitmq.listener.order.exchange.durable}" ,
ignoreDeclarationExceptions = "${spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions}"),
key = "${spring.rabbitmq.listener.order.key}"
)
)
public void onOrderMessage(@Payload OrderMessageDto messageDto, @Headers Map<String,Object> headers, Channel channel) throws Exception{
log.info("message : {}",messageDto.toString());
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
if(messageDto.getOrderNumber().equals("")){
/**
* multiple 是否批量处理
* requeue 是否重回队列
* 将消息重新投递回broker中队列的尾端( 重回队列)
*/
channel.basicNack(deliveryTag,false,true);
}else{
//手工ack 确认签收
channel.basicAck(deliveryTag,false);
}
}
死信队列: DLX ,Dead-Letter-Exchange
利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange 就是DLX
DLX也是一个正常的Exchange,和普通的Exchange没有区别,他能在任何的队列上被指定,实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列中,可以监听这个队列中的消息做相应的死信处理
死信:在队列中没有任何消费者消费的消息就是死信
消息变成死信的情况
1.消息被拒绝( basic.reject / basic.nack ) 并且request= false
消息消费失败并且不重回队列
2.消息的TTL过期
3.当队列达到最大长度
死信队列的设置
首先设置死信队列的Exchange 和 Queue,然后进行绑定
例如 : Exchange :dlx.exchange 、Queue:dlx.queue 、 RoutingKey:# (路由任何规则)
然后正常的队列中添加参数( “x-dead-letter-exchange”, “dlx.exchange” )
这样消息在过期,消息被拒绝,队列到达最大长度的时候 消息就可以直接路由到死信队列,我们只需要监听死信队列的queue即可
创建死信队列
@RabbitHandler
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(
value = "${spring.rabbitmq.listener.order.queue.name}",
durable = "${spring.rabbitmq.listener.order.queue.durable}",
arguments = {
@Argument(name="x-expires",value = "20000",type = "java.lang.Integer"),
@Argument(name="x-message-ttl",value = "10000",type = "java.lang.Integer"),
@Argument(name="x-dead-letter-exchange",value = "exchange.dlx")
}
),
exchange = @Exchange(
value = "${spring.rabbitmq.listener.order.exchange.name}" ,
type = "${spring.rabbitmq.listener.order.exchange.type}" ,
durable = "${spring.rabbitmq.listener.order.exchange.durable}" ,
ignoreDeclarationExceptions = "${spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions}"),
key = "${spring.rabbitmq.listener.order.key}"
)
)
public void onOrderMessage(@Payload OrderMessageDto messageDto, @Headers Map<String,Object> headers, Channel channel) throws Exception{
log.info("message : {}",messageDto.toString());
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
if(messageDto.getOrderNumber().equals("")){
/**
* multiple 是否批量处理
* requeue 是否重回队列
* 将消息重新投递回broker中队列的尾端( 重回队列)
*/
channel.basicNack(deliveryTag,false,true);
}else{
//手工ack 确认签收
channel.basicAck(deliveryTag,false);
}
}