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 区分不同的队列

 

Fanout(广播)

将接收到的所有的消息广播到所有队列中

绑定交换机和队列

 

Channel.queueBind(queueName,Exchange_NAME,“”);//第三个参数 routingKey

两个队列的Routingkey相同 将都接收到消息

 

direct交换机

routingkey模式 路由模式

声明队列的时候 指明交换机是direct类型

direct_logs ->console ->nfo

                   ->console ->warming(多重绑定)

                   ->disk ->error

一个队列,拥有不同的routing key 多重绑定

谁能接收到消息 完全取决于rouingkey

 

topic交换机 

Routingkey不同,直接交换机只能给一个队列发消息

主题交换机的routing key 必须是一个单词列表 以点号分割

 “quick,orange.rabbit” 不能超过255个字节

(*.orange.*)匹配三个单词中间是orange

(lazy.#)#匹配多个单词

 

当一个队列绑定键是#,那么这个队列将接收所有的数据,类似于fanout

当队列绑定键中没有#和*出现,那这个队列绑定类型类似于direct

 

死信队列

消息无法被消费

某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信

应用场景:为了保证订单业务的消息数据不丢失,需要用到死信队列机制。

当消息发生异常,将消息投入死信队列。

支付超时未付款订单会自动失效

 

死信的来源

消息ttl过期(存活时间)

队列达到最大长度

消息被拒绝

 

死信消息

//通过参数转发消息到死信队列

HashMap<String, Object> arguments = new HashMap<>();

//过期时间

//正常队列设置死信交换机

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);

 

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时间

发送消息的时候设置过期时间

发送多条信息会排队,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集群的特性 可以解决单点故障的问题 不会因为单个节点挂掉导致延时队列不可用或者消息丢失。