mq
消息队列 先进先出
1.为什么要使用mq?
异步 削峰 解耦
1.流量削峰
使用消息队列做一个缓冲
2.应用解耦
可以解决系统之间的调用问题。如果物流系统出现故障,需要几分钟修复,通过消息队列作为中间件,在这几分钟内,物流系统要处理的内存被缓存在消息队列中,用户可以正常下单。
缺点
3.异步处理
A调用B 只需要监听b处理完成的消息,B处理完成之后,会发送一条消息给MQ ,MQ会将这条消息转给A服务。
mq的种类
ActiveMQ
单机吞吐量高 时效性ms级,可用性高,消息可靠性高
官方社区对其维护越来越少,高吞吐量场景较少使用
Kafka
大数据领域内的消息传输 百万级别吞吐量
优点 吞吐量高 时效期ms级,分布式 少数机器宕机,不会导致不可用,消息有序,能保证所有消息被消费且只能消费一次 在日志领域比较成熟
主要用于大数据领域的实时计算以及日志采集
缺点:消息失败不支持重试 单机超过64个分区,load(CPU)会发生明显的飙高
采用短轮询方式,实时性取决于轮询间隔时间
一台代理宕机,会产生乱序
Rocketmq
订单 交易 充值 日志流式处理
优点:单机吞吐量十万级 可用性高 分布式 消息可以做到0丢失 扩展性好 支持大量数据的数据堆积
缺点;支持语言少 支持java和c++
Rabbitmq
由于erlang的高并发性,吞吐量到万级,支持多种语言,开源,提供了管理页面,社区活跃性高
缺点;商业版需要收费
mq的选择
Kafka 大量数据的互联网公司
Rocketmq 金融互联网
Rabbitmq 中小型公司
Rabbitmq
接收 存储 转发消息
Rabbitmq
接收 存储 转发消息
生产者 交换机 队列 消费者
六大模式
简单模式 工作模式 发布订阅模式 路由模式 主题模式 发布确认模式
Broker 接收和分发消息的应用 mq的服务器
-exchange
-quenue
Channel 信道
连接里面多个信道 减少建立连接的开销
Broker 里面有多个virtual host 每个用户在自己的vhost创建exchange/queue
简单模式
一个消费者 mq 一个生产者
工作模式
工作队列的主要思想是避免立即执行资源密集型任务,而不得不等待它完成,相反我们安排任务在之后完成。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程,这些工作线程将一起处理这些任务。
生产者大量发消息给队列,造成很多消息停留在队列中,无法进行及时处理。通过多个工作线程,采用轮询的方式来处理。
消费者-》多个工作线程。轮询 竞争关系
一个消息只能被处理一次 不可以处理多次
消息应答
问题:
某个消费者处理一个长的工作任务并且仅完成了部分就突然挂掉了。rabbitmq一旦向消费者发送了某条消息,就立即将消息设置为删除。这种情况下,我们将会丢失正在处理的消息,以及后续发送给该消费者的消息,它将无法接收。
消息应答:消费者接收到消息并处理完消息之后,告诉rabbitmq消息已经处理了,rabbitmq可以把消息删除了。
自动应答
高吞吐量和数据传输安全要有保证
手动应答
手动应答的方法
basicAck 肯定确认(如果批量应答 是否批量 true)
basicNack 否定确认 比另一个多一个参数。是否批量
basicReject 否定确认
批量应答 最好别 multiple
消息的自动重新入队
消息未发送ACK确认 会重新入队 rabbitmq会安排另一个消费者处理
消息手动应答时是不丢失的 放回队列中重新消费
//手动应答
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
rabbitmq持久化
确保消息不丢失 队列和消息持久化
1.队列持久化 durable true
boolean durable=true;
channel.queueDeclare(normal_queue,durable,false,false,arguments);
队列不是持久化的 需要把原来的队列先删除掉 或者重新创建一个持久化的队列 不然会报错
2.消息持久化
生产者发消息时通知mq消息持久化 MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublish("",task_queue_name, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes(“UTF-8"));
不公平分发
channel.basicQos(1);
消费者接收消息之前设置不公平分发
预取值
指定消费者分多少条消息
prefetchCount 5
prefetchCount 2
channel.basicQos(prefetchCount);
如果超过7条 按照不公平分发
发布确认原理
生产者
设置队列必须持久化
设置要求队列中的消息必须持久化
发布确认 :mq 把消息保存到磁盘上 ,保存成功后 通知生产者
1) 单个确认发布
发布速度特别慢 如果没有确认发布的消息就会阻塞后续所有消息的发布
channel.confirmSelect();
channel.waitForConfirms() //
2) 批量确认发布
当发生故障导致发布出现问题时,不知道是哪个消息出现问题了。
3) 异步确认发布. 利用回调函数 保证是否投递成功
如何处理异步确认中确认失败的消息?把未确认的消息放到一个基于内存的能被发布线程访问的队列
//异步确认 public static void publishMessageAsync() throws Exception{ Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName,true,false,false,null); //发布确认 //线程安全有序的一个哈希表 适用于高并发的情况下 /** * 1.轻松将序号和消息关联 * 2。轻松批量删除条目 只要给到序号 * 3。支持高并发 */ ConcurrentSkipListMap<Long,String> outstandingConfirms =new ConcurrentSkipListMap<>(); //消息确认成功 ConfirmCallback ackCallback=(deliveryTag, multiple) -> { //删除掉已经确认成功的消息 if(multiple){ ConcurrentNavigableMap<Long, String> confimed= outstandingConfirms.headMap(deliveryTag); confimed.clear(); //批量 }else{ outstandingConfirms.remove(deliveryTag); //单个 } System.out.println("确认成功的消息"+deliveryTag); }; //消失确认失败 ConfirmCallback nackCallback=(deliveryTag, multiple) -> { String message = outstandingConfirms.get(deliveryTag); System.out.println("确认失败的消息"+message); }; channel.confirmSelect(); //准备消息的监听器 channel.addConfirmListener(ackCallback,nackCallback); int batch =1000; long start = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message=i+""; channel.basicPublish("",queueName,null,message.getBytes()); //记录下所有的消息 outstandingConfirms.put(channel.getNextPublishSeqNo(), message); } long end = System.currentTimeMillis(); System.out.println("发布1000个单独确认需要时间:"+(end-start)); }
交换机
发布订阅模式 一个消息想被多个消费者消费
生产者-消息- 交换机 -routingkey-队列- 消息只能被消费一次-消费者
-routingkey-队列-消息只能被消费一次 -消费者
生产者只能将消息发送到交换机,交换机一方面接收来自生产者的消息,另一方面将它们推入队列,
exchange
直接direct 主题topic 标题headers(不常用) 扇出fanout
“”表示无名或者默认交换机
routingkey 绑定key 指定交换机
临时队列
不带有持久化 名字是随机的 队列 一旦断开了消息者的队列,队列将被自动删除。
String queueName=Channel.queueDeclare().getQueue();
绑定 (交换机 queue 之间的桥梁)
通过routingkey进行绑定
通过routing key 区分不同的队列
1)Fanout(广播) 发布订阅模式。 扇出
将接收到的所有的消息广播到所有队列中
绑定交换机和队列
Channel.queueBind(queueName,Exchange_NAME,“”);//第三个参数 routingKey
两个队列的Routingkey相同 将都接收到消息
2)direct交换机 路由模式 routingkey模式
声明队列的时候 指明交换机是direct类型
生产者-消息- 交换机 -routingkey-队列- 消息只能被消费一次-消费者
-routingkey-队列-消息只能被消费一次 -消费者
routingkey相同是扇出交换机 不同是直接交换机
direct_logs 交换机->console 队列 ->nfo routingkey
- >console 队列->warming(多重绑定)
->disk 队列->error
一个队列,拥有多个routing key 多重绑定
谁能接收到消息 完全取决于rouingkey
routingkey->info console接收
routingkey-> warming console接收
routingkey->error disk接收
3)topic交换机
Routingkey不同,直接交换机只能给一个队列发消息
主题交换机的routing key 必须是一个单词列表 以点号分割
“quick,orange.rabbit” 不能超过255个字节
(*.orange.*)匹配三个单词中间是orange
(lazy.#)#匹配多个单词
当一个队列绑定键是#,那么这个队列将接收所有的数据,类似于fanout
当队列绑定键中没有#和*出现,那这个队列绑定类型类似于direct
死信队列
消息无法被消费
某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信
应用场景:为了保证订单业务的消息数据不丢失,需要用到死信队列机制。
当消息发生异常,将消息投入死信队列。
支付超时未付款订单会自动失效
死信的来源
消息ttl过期(存活时间)
队列达到最大长度
消息被拒绝
生产者 -普通交换机 type=direct->普通队列—-》c1
|
消息TTL过期
队列达到最大长度 成为死信
消息被拒绝
|
死信交换机 type=direct
|
死信队列———》c2
死信消息
//通过参数转发消息到死信队列
HashMap<String, Object> arguments = new HashMap<>(); //过期时间. ms arguments.put("x-message-ttl",100000); //正常队列设置死信交换机 arguments.put("x-dead-letter-exchange",dead_exchange); //死信routingkey arguments.put("x-dead-letter-routing-key","lisi"); channel.queueDeclare(normal_queue,false,false,false,arguments);ggu p
设置过期时间:
一种在普通队列 设置过期时间
另一种在生产方发消息时设置过期时间。(比较灵活 可以随意修改过期时间)
发消息时设置过期时间:props 设置死信消息的过期时间
AMQP.BasicProperties properties =new AMQP.BasicProperties().builder().expiration("1000").build();
设置死信队列的长度
arguments.put("x-max-length",6);
超过部分会成为死信消息
消息被拒绝 指定某条消息被拒绝
需要开启手动应答
if(message.equals(“info5")){
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);//(消息的标志,是否放回队列)
}
延迟队列(死信队列中ttl过期)
队列内部是有序的
在某个事件发生之前或者之后的指定时间完成某一项任务
订单十分钟内未支付则关闭
整合springboot:跳过
延迟队列:延迟指定时间消费消息
优化
每新增一个时间需求,就要新增一个队列
QA QB指定了过期时间。【QC不指定过期时间 没设置ddl时间】
解决方法:发送消息的时候设置过期时间
rabbitTemplate.convertAndSend(“X”,”XC”,message,msg->{ msg.getMessageProperties().setExpiration(ttlTime); return msg; });
基于死信的延迟存在问题:
发送多条信息会排队,rabbitmq只会检查第一个队列,如果第一个消息的延时时长很长,第二个消息的延时时长很短,第二条消息并不会得到优待。
基于延迟消息插件的延迟队列:延迟交换机 x-delayed-message
生产者 -》延迟交换机 -〉队列 -》消费者
声明一个延迟交换机 基于插件的延时队列
public CustomExchange delayedExchange(){ Map<String,Object> arguments =new HashMap<>(); arguments.put("x-delayed_type","direct"); return new CustomExchange(delayed_exchange_name,"x-delayed-message",true,false,arguments); }
延时队列:使用rabbitmq实现延时队列可以很好的利用rabbitmq的特性,消息的可靠发送,可靠投递,利用死信队列保证消息至少被消费一次 以及未被正确处理的消息不会被丢弃。
Rabbitmq集群的特性 可以解决单点故障的问题 不会因为单个节点挂掉导致延时队列不可用或者消息丢失。