消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
1 订阅模型
Fanout(广播): 每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的
Direct(直连): 消息中的路由键(routing key)如果和 Binding 中的 binding key 一致,交换器就将消息发到对应的队列中。它是完全匹配、单播的模式。
topic(主题): Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
“#” 匹配 0 个或多个单词,“*”匹配不多不少一个单词
Headers: 不处理路由键。而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型。而fanout,direct,topic 的路由键都需要要字符串形式的。
2 消息丢失分类
- 生产者丢失: 生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能
- MQ中丢失: 就是 RabbitMQ 自己弄丢了数据
- 消费端丢失: 你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
2.1 生产者端丢失消息
(不推荐) 一种方法是用RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ事务channel.txSelect
,然后发送消息,如果消息没有成功被RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback
,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit
。吞吐量会下来,因为太耗性能。而且事务机制是同步的,你提交一个事务之后会阻塞在那儿。
最常用的方法是开启confirm模式:以Spring AMQP为例,可以实现RabbitTemplate.ConfirmCallback和RabbitTemplate.ReturnCallback接口来完成生产者确认。
- 如果消息没有到exchange,则confirm回调,ack=false
- 如果消息到达exchange,则confirm回调,ack=true
- exchange到queue成功,则不回调return
- exchange到queue失败,则回调return
@Configuration
@Slf4j
public class ProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this); //指定 ConfirmCallback
rabbitTemplate.setReturnCallback(this); //指定 ReturnCallback
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息发送成功:" + JSON.toJSONString(correlationData));
} else {
log.info("消息发送失败:{} 数据:{}", cause, JSON.toJSONString(correlationData));
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
// 反序列化对象输出
System.out.println("消息主体: " + new String(message.getBody()));
System.out.println("应答码: " + replyCode);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交换器 exchange : " + exchange);
System.out.println("消息使用的路由键 routing : " + routingKey);
}
}
2.2 MQ中丢失消息
开启交换机和队列的持久化
在@Queue和@Exchange注解中都有autoDelete属性,值是布尔类型的字符串。如:autoDelete=“false”
。
-
@Queue:当所有消费客户端断开连接后,是否自动删除队列: true:删除,false:不删除。
-
@Exchange:当所有绑定队列都不在使用时,是否自动删除交换器: true:删除,false:不删除。
-
durable: 参数为false,消息在重启rabbitmq后丢失;修改为true后重新启动项目
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = "seckillExchange", type = "topic",durable = "true",autoDelete="false"),
value = @Queue(value = "seckillQueue",durable = "true",autoDelete="false"),
key = "seckill.message"
))
持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
2.3 消费端丢失
这个时候得用 RabbitMQ 提供的ack机制,简单来说,就是你关闭 RabbitMQ 的自动ack,设置为手动ack
spring.rabbitmq.listener.simple.acknowledge-mode: manual
手动确认模式
确认消息:
// 参数二:是否批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
拒绝消息:
// 参数二:是否重新入队,false时消息不再重发,如果配置了死信队列则进入死信队列,没有死信就会被丢弃
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
不确认消息:
// 参数二:是否批量; 参数三:是否重新回到队列,true重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
3 死信队列
死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?
“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
- 消息被否定确认,使用
channel.basicNack
或channel.basicReject
,并且此时requeue
属性被设置为false
。 - 消息在队列的存活时间超过设置的TTL时间。
- 消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。
4 延时队列
延时队列,最重要的特性就体现在它的延时属性上,延时队列中的元素则是希望被在指定时间得到取出和处理,所以延时队列中的元素是都是带时间属性的,通常来说是需要被处理的消息或者任务。
简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
比如以下场景:
- 订单在十分钟之内未支付则自动取消。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。