RabbitMQ

RabbitMQ
1、MQ介绍
(1)本质是 队列 ,先进先出,队列中存放的是message,是一种 跨进程的通信机制。在互联网架构中,MQ是一种十分常见的上下游“ 逻辑解耦+物理解耦”的消息通信服务
(2)MQ常用功能:
    1) 流量削峰 :假设订单系统超过1万/秒访问,服务就会宕机。可使用MQ使请求进行排队,虽然会影响性能,但可保护服务不宕机。
    2) 解耦 :微服务调用链中某个服务出现异常可能导致前置服务也出现异常。
    3) 异步处理
(3)MQ分类
    1)ActiveMQ:吞吐量高,失效ms级,消息可靠性高。但已停止维护。
    2) Kafka大数据杀手锏 。吞吐量高,时效性ms级,且是分布式的,一个数据多个副本,少数服务宕机,不会丢失数据。消息有序,通过控制能保证所有消息被消费且仅消费一次,也可用于日志采集。缺点:Kafka单机超过64个队列/分区,load明显飙高。消费失败不支持重试;支持消息顺序,但一台代理宕机,就会产生消息乱序。
    3) RocketMQ :阿里开源,在设计时参考了Kafka并进行改进。单机吞吐量十万级, 可用性非常高。分布式架构, 消息可做到0丢失 ,支持10亿级别的消息堆积。支持的客户端语言不多。
    4) RabbitMQ :在 AMQP (高级消息队列协议)基础上完成,是当前最主流的消息中间件之一。并发性高,吞吐量万级,支持多语言、 跨平台 。商业版需要收费,学习成本高。
(4)MQ的选择
    1) Kafka大数据量、日志采集功能
    2) RocketMQ金融互联网 ,可靠性高,高并发场景
    3) RabbitMQ :数据量没那么大,中小型公司
(5)RabbitMQ:负责消息接收、存储、转发。四大核心理念如下:
    1)生产者
    2)交换机
    3)队列
    4)消费者
(6) RabbitMQ核心部分
    1)HelloWorld 简单模式
    2)WorkQueues  工作模式
    3)Publish/Subcscribe  发布订阅模式
    4)Routing  路由模式
    5)Topics  主题模式
    6)Publisher Confirms  发布确认模式
(7) RabbitMQ原理:
    1)Broker:接收和分发消息的应用。包含Exchange和Queue
    2)Connection:publisher / consumer和broker之间的TCP连接
    3)Channel:如果每次访问RabbitMQ都建立一个 Connection,在消息量大的时候建立TCP Connection的消耗是巨大的。Channel是在 Connection内部建立的逻辑连接 ,若应用程序支持多线程,通常每个Thread创建单独的channel进行通讯,通过channel id进行识别,保证信道完全隔离。Channel作为轻量级的Connection极大 减少了操作系统建立TCP连接的开销
    4)Exchange:根据分发规则,匹配查询表中的routing key,分发到消息队列Queue中
    5)Queue:消息最终被送到这里等待Consumer取走
    6)Binding:Exchange和Queue之间的虚拟连接,Binding信息被保存到Exchange的查询表中,用于message的分发依据
(8)Java代码实现消息生产者
ConnectionFactory factory= newConnectionFactory();
factory.setHost();
factory.setUsername();
factory.setPassword();
Connection connection = factory.newConnection();
Channel channel = connection.creatChannel();
/**
*生成一个队列
*1、队列名称
*2、队列里消息是否支持持久化,默认消息存储在内存中
*3、该队列消息是否支持共享
*4、是否自动删除:最后一个消费者断开连接后,该队列是否自动删除
*5、其他参数
channel.queueDeclare("hello",false,flase,false,null);
/**发送消息
*1、发送到哪个交换机
*2、路由key是哪个  本次队列名称
*3、其他参数信息
*4、消息内容
*5、其他参数
channel.basePublish("","hello",null,message.getBytes());
(9)Java代码实现消息消费者
ConnectionFactory factory= newConnectionFactory();
factory.setHost();
factory.setUsername();
factory.setPassword();
Connection connection = factory.newConnection();
Channel channel = connection.creatChannel();
/**消费消息
*1、消费哪个队列
*2、消费成功之后是否自动应答
*3、消费未成功回调方法
*4、消息者取消消费回调方法
*/
channel.basicConsume("hello",true,deliverCallback,cancelCallback);
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
    System.out.println(new String(message.getBody()));
};
// 声明 取消消息
CancelCallbackcancelCallback= consumerTag -> {
    System.out.println("消息被中断");
};
10、工作队列原理:生产者大量发送消息,需要多个工作线程接收消息进行处理。
(1)注意事项:一个消息只能被处理一次,不可以处理多次。所以工作线程之间是竞争关系,采用轮询的方式分发消息。
(2)代码实现:
//  发送消息
Scanner scanner = new Scanner(Syetem.in);
while(scanner.hasNext()){
    String message = scanner.next();
    channel.basePublish("","hello",null,message.getBytes());
}
11、消息应答:工作线程耗时长,如果消费者在处理一个长的任务且仅完成部分时突然挂掉了,则会造成消息丢失。为解决此问题,rabbitMQ引入了消息应答机制:消费者在接收到消息并且处理该消息之后,告诉rabbitMQ他已经处理了,rabbitMQ可以把消息删除了。
(1)自动应答:并不完善,需要在吞吐量和安全性两方面进行权衡,接收到消息便进行应答,尽量不用。
(2) 手动应答:消息手动应答时,是不会丢失的。断开连接等问题会将消息重新放入队列进行消费
    1)Channel.basicAck();用于肯定确认
    2)Channel.basicNack();用于否定确认
    3)Channel.basicReject();用于否定确认,不处理该消息了,可以直接丢弃
(3)multiple的解释:批量应答
channel.basicAck(deliveryTag,false);
第二个参数表示是否进行批量应答。尽量使用false避免批量应答造成消息丢失
(4)消息自动重新入队:如果消费者由于某些原因失去连接,导致消息未发送ACK确认,RabbitMQ将了解到消息为完全处理,并将其重新排队。所以即使某个消费者偶尔死亡消息也不会丢失。
(5)消息手动应答代码:
/**消费消息
*1、消费哪个队列
*2、消费成功之后是否自动应答
*3、消费未成功回调方法
*4、消息者取消消费回调方法
*/
boolean ackType = false;// 手动应答
channel.basicConsume("hello",ackType,deliverCallback,cancelCallback);
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
    System.out.println(new String(message.getBody()));
    channel.basicAck(delivery.getEnvelope().getDeliverytag(),false);
};
// 声明 取消消息
CancelCallbackcancelCallback= consumerTag -> {
    System.out.println("消息被中断");
};
12、 消息队列持久化
(1)上述 手动应答机制可以保证处理任务时不丢失的情况,但是如何保证当rabbitMQ服务停掉之后消息生产者法送过来的消息不丢失。默认情况下,RabbitMQ退出或崩溃时会忽视队列消息。除非 修改配置将队列和消息都标记为持久化
(2)声明队列时,持久化标记为TRUE
/**
*生成一个队列
*1、队列名称
*2、队列里消息是否支持持久化,默认消息存储在内存中
*3、该队列消息是否支持共享
*4、是否自动删除:最后一个消费者断开连接后,该队列是否自动删除
*5、其他参数
channel.queueDeclare("hello",true,flase,false,null);
(3)消息持久化
    1)需要在消息发布时指定策略:
/**发送消息
*1、发送到哪个交换机
*2、路由key是哪个  本次队列名称
*3、其他参数信息
*4、消息内容
*5、其他参数
channel.basePublish("","hello",MessageProperties.PERSSTENT_TEXT_PLAIN,message.getBytes());
    2)注意事项:将消息标记为持久化并不能完全保证不会丢失,对于简单任务队列已满足。若还需要更强有力的持久化策略,需参考发布确认功能。
13、 消息不公平分发:能者多劳
(1)需要 在消费消息时,进行参数设置
/**消费消息
*1、消费哪个队列
*2、消费成功之后是否自动应答
*3、消费未成功回调方法
*4、消息者取消消费回调方法
*/
channel.basicConsume("hello",true,deliverCallback,cancelCallback);
channel.setBasicQos(1);// 设置消息不公平分发,能者多劳
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
    System.out.println(new String(message.getBody()));
};
// 声明 取消消息
CancelCallbackcancelCallback= consumerTag -> {
    System.out.println("消息被中断");
};
14、预取值,手动指定消费者消费数量:
/**消费消息
*1、消费哪个队列
*2、消费成功之后是否自动应答
*3、消费未成功回调方法
*4、消息者取消消费回调方法
*/
channel.basicConsume("hello",true,deliverCallback,cancelCallback);
channel.setBasicQos(5);// 设置消息预取值
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
    System.out.println(new String(message.getBody()));
};
// 声明 取消消息
CancelCallbackcancelCallback= consumerTag -> {
    System.out.println("消息被中断");
};
15、 发布确认——>可以解决消息不丢失问题
(1)设置要求 队列必须持久化
(2)设置要求 队列中消息必须持久化
(3)发布确认:在磁盘中 完成持久化后通知生产者
(4)发布确认的策略
    1)开启发布确认,发布确认默认是关闭的
Channel channel = connection.creatChannel();
channel.confirmSelect(); // 开启发布确认
//生成一个队列
channel.queueDeclare("hello",false,flase,false,null);
//发送消息
channel.basePublish("","hello",null,message.getBytes());
    2)单个发布确认:同步确认方式,发布速度特别慢,吞吐量不超过百条
public static void single(){
    Channel channel = RabbiyMQUtil.getChannel();
    String queueName = UUID.randomUUID.toString();
    channel.queueDeclare(queueName,false,true,false,null);
    channel.sonfirmSelect(); // 开启发布确认
    for(int i=0;i<=1000;i++){
        String message = i+"";
        channel.basicPublish("",queueName,null,message.getBytes());
        boolean flag = channel.waitForConfirms(); // 单个消息就马上进行发布确认
        if(flag){
            System.out.println("消息发送成功");
        }
    }
}
    3)批量发布确认:同步确认方式,吞吐量较大,但是发生故障时,不知道是哪个消息出现故障
public static void batch(){
    Channel channel = RabbiyMQUtil.getChannel();
    String queueName = UUID.randomUUID.toString();
    channel.queueDeclare(queueName,false,true,false,null);
    int batchSize = 100;//定义批量步长
    channel.sonfirmSelect(); // 开启发布确认
    for(int i=0;i<=1000;i++){
        String message = i+"";
        channel.basicPublish("",queueName,null,message.getBytes());
        if(i%batchSize == 0){
            channel.waitForConfirms(); // 每100条消息进行发布确认
        }
    }
}
    4) 异步发布确认:性价比最高,可靠性和效率都满足要求。编码逻辑比较复杂。 通过回调函数进行实现。
    broker接收到异步发送的消息后,会通过ackCallback()回调函数确认收到消息;通过n ackCallback()回调函数确认未收到消息。
public static void async(){
    Channel channel = RabbiyMQUtil.getChannel();
    String queueName = UUID.randomUUID.toString();
    channel.queueDeclare(queueName,false,true,false,null);
    channel.sonfirmSelect(); // 开启发布确认
    /**
     *线程安全的有序哈希表
     *1、轻松将序号与消息关联
     *2、轻松进行批量删除
     *3、支持高并发
     */
    ConcurrentSkipListMap<Long,String> confirm = new ConcurrentSkipListMap();
    // 准备消息的监听器,监听哪些消息成功了   哪些消息失败了
    channel.addConfirmListener(ackCallback, nackCallback);
    // 消息成功回调函数
    // deliveryTag:消息的标记   multiple:是否批量确认
    ConfirmCallback ackCallback = (deliveryTag,multiple) -> {
        // 删除已经确认的消息
        if(multiple){
            ConcurrentSkipListMap<Long,String> confirmed = confirm.headMap(deliveryTag);
            confirmed.clear();
        }else {
            confirm.remove(deliveryTag);
        }
    };
    // 消息失败回调函数
    ConfirmCallback nackCallback = (deliveryTag,multiple) -> {
        // 打印未确认的消息
    };
    for(int i=0;i<=1000;i++){
        String message = i+"";
        channel.basicPublish("",queueName,null,message.getBytes());
        // 在此处记录所有的消息
        confirm.put(channel.getNextPublishSeqNo(),message);
    }
}
5)如何处理异步未确认消息
方案:最好的方案就是把未确认的消息放到能被发布线程访问的内存队列中,比如: ConcurrentLinkedQueue,这个队列在confirmcallbacks与发布线程之间进行传递
16、 交换机:默认AMQP default交换机
(1)RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不直接发送到队列,生产者只将消息发送到交换机。交换机负责接收消息并将消息根据规则推入队列。
(2)交换机类型: 直接(路由类型)、主题、标题、扇出(发布订阅模式)
(3)临时队列:不进行持久化
(4)binding绑定:交换机根据Routingkey将消息推送至指定的Queue,消费者获取指定的消息
(5) Fanout(扇出/发布订阅):消息可以被多个消费者消费
// 消费者
Channel channel = RabbiyMQUtil.getChannel();
public static final String exchangeName = "logs"; 
// 声明 交换机
channel.exchangeDeclare(exchangeName, "fanout");
// 生成一个临时队列
String queueName = channel.queueDeclare().getQueue();
// bingding  第三个参数为Routingkey
channel.queueBind(queueName,exchangeName,"");
channel.basicConsume(exchangeName,true,deliverCallback,null);
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
    System.out.println(new String(message.getBody()));
};
// 生产者
Channel channel = RabbiyMQUtil.getChannel();
public static final String exchangeName = "logs";
// 声明 交换机
channel.exchangeDeclare(exchangeName, "fanout");
// 生成一个临时队列
String queueName = channel.queueDeclare().getQueue();
// bingding  第三个参数为Routingkey
channel.queueBind(queueName,exchangeName,"");
//  发送消息
Scanner scanner = new Scanner(Syetem.in);
while(scanner.hasNext()){
    String message = scanner.next();
    channel.basicPublish(exchangeName,"",null,message.getBytes());
}
(6)direct交换机:与fanout相比routing key值不相同,所以也叫做路由交换机。直接交换机只能路由某一个队列。
// 声明 交换机
channel.exchangeDeclare(exchangeName, "direct");
(7)Topic交换机: 主题交换机的Routingkey不能随意写,必须满足一定规则:必须是一个单词列表,以点号分开:stock.usd.nyse, *可代替一个单词,#可代替多个单词
    1)当一个队列绑定的键是#,那么这个队列将接收所有数据,就有点类似fanout扇出/发布订阅模式
    2)当一个队列绑定的键没有#和*,那么该队列就是direct交换机模式了
    3)topic交换机实战:
// 声明 交换机
channel.exchangeDeclare(exchangeName, "topic");
16、 死信队列
(1)由于特定原因导致queue的的某些消息无法被消费,这些消息如果没有后续处理,就变成了死信,也就产生死信队列。
(2)应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当 消息发生异常时,将消息投入死信队列
        用户在商城下单成功并点击支付后在指定时间内未支付导致自动失效
(3)死信的来源:
    消息TTL过期(存活时间):10秒内未被消费
     队列达到最大长度
     消息被拒绝且requeue=false
(4)实战-消息达到过期时间导致死信
// 消费者1
// 普通交换机名称
public static final String normalExchange = "normalExchange";
// 死信交换机名称
public static final String deadExchange = "deadExchange";
// 普通队列名称
public static final String normalqueue= "normalqueue";
// 死信队列名称
public static final String deadqueue = "deadqueue";
Channel channel = RabbiyMQUtil.getChannel();
// 声明 普通交换机
channel.exchangeDeclare(normalExchange, "direct");
// 声明 死信交换机
channel.exchangeDeclare(deadExchange, "direct");
// 声明 普通队列   必须设置参数,才能将消息转发至死信交换机
Map<String,Object> argument = new HashMap<>();
//设置过期时间    一般在生产者发送消息时设置过期时间
argument.put("x-message-ttl",10*60*1000);
// 填写死信交换机
argument.put("x-dead-letter-exchange",deadExchange);
// 设置死信Routingkey
argument.put("x-dead-letter-routing-key",""lisi);
// 设置正常队列的长度限制
arguments.put("x-max-length",6);
channel.queueDeclare(normalqueue,false,false,false,argument);
// 声明 死信队列
channel.queueDeclare(deadqueue,false,false,false,null);
// 绑定普通交换机和普通队列
channel.queueBind(normalqueue,normalExchange,"zhangsan");
// 绑定死信交换机和死信队列
channel.queueBind(deadqueue,deadExchange,"lisi");
channel.basicConsumer(normalqueue,true,deliverCallback,consumerTag -> {});
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
    System.out.println(new String(message.getBody()));
};
// 生产者
Channel channel = RabbiyMQUtil.getChannel();
// 死信消息   设置TTL时间  time to live
AMQP.BasicProperties  properties = new  AMQP.BasicProperties().build().expiration("10*1000").build();
for(int i=0;i<=10;i++){
    channel.basicPublish(normalExchange,"zhangsan",properties,message.getBytes());
}
(5)实战-消息超过队列最大长度导致死信消息
// 消费者1
// 设置正常队列的长度限制
arguments.put("x-max-length",6);
(6)实战-消息被拒绝导致死信消息
// 声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message) -> {
    if("info".equals(message.getBody())){
        // 拒绝接收消息并且不在将消息放回普通队列
        channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
    }else{
        System.out.println(new String(message.getBody()));
        // 接收消息并进行应答,不进行批量应答
        channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
    }
};
channel.basicConsumer(normalqueue,false,deliverCallback,consumerTag -> {});//开启手动应答
17、 延迟队列——>基于上述死信队列中,消息超时
(1)延时队列就是用来存放需要在指定时间被处理的元素的队列
(2)延迟队列使用场景
    1)订单十分钟内未支付自动取消
    2)新创建的店铺,十天内未上传过商品,发消息提醒
    3)用户注册成功后,如果三天没有登录则进行短信提醒
    4)用户发起退款,如果三天没有处理则发消息通知运维人员
    5)预定会议后,需要在预定时间点前十分钟通知参会人员
    采用定时任务轮询的方式进行处理不适用于数据量大、时效性强的场景。
(3)整合springboot实现延迟队列
    1)配置文件类 进行声明
@Configration
public class TtlQueueConfig{
    // 普通交换机名称
public static final String XExchange = "X";
// 死信交换机名称
public static final String YDeadExchange = "Y";
// 普通队列名称
public static final String aqueue= "QA";
// 普通队列名称
public static final String bqueue= "QB";
// 死信队列名称
public static final String deadqueue = "QD";
// 声明 普通交换机
@Bean("XExchange")
public DirectExchange xExchange(){
    return new DirectExchange(XExchange);
}
// 声明 死信交换机
@Bean("YDeadExchange")
public DirectExchange xExchange(){
    return new DirectExchange(YDeadExchange);
}
// 声明 普通队列QA :过期时间10秒
@Bean("aqueue")
public Queue queueA(){
    Map<String,Object> argument = new HashMap<>();
    //设置过期时间    单位ms
    argument.put("x-message-ttl",10*1000);
    // 填写死信交换机
    argument.put("x-dead-letter-exchange",YDeadExchange);
    // 设置死信Routingkey
    argument.put("x-dead-letter-routing-key","YD");
    return QueueBuilder.durable(aqueue).withArguments(argument).build;
}
// 声明 普通队列QB :过期时间40秒
@Bean("bqueue")
public Queue queueB(){
    Map<String,Object> argument = new HashMap<>();
    //设置过期时间    单位ms
    argument.put("x-message-ttl",40*1000);
    // 填写死信交换机
    argument.put("x-dead-letter-exchange",YDeadExchange);
    // 设置死信Routingkey
    argument.put("x-dead-letter-routing-key","YD");
    return QueueBuilder.durable(bqueue).withArguments(argument).build();
}
// 声明 死信队列QD
@Bean("dqueue")
public Queue queueD(){
    return QueueBuilder.durable(deadqueue).build();
}
@Bean
public Binding queueABindingX(@Qualifier("aqueue") Queue queueA,
    @Qualifier("XExchange") Exchange exchange){
    return BindingBuilder.bind(queueA)to(exchange).with("XA");
}
@Bean
public Binding queueBBindingX(@Qualifier("bqueue") Queue queueB,
    @Qualifier("XExchange") Exchange exchange){
    return BindingBuilder.bind(queueB)to(exchange).with("XB");
}
@Bean
public Binding queueDBindingY(@Qualifier("dqueue") Queue queueD,
    @Qualifier("YDeadExchange") Exchange exchange){
    return BindingBuilder.bind(queueD)to(exchange).with("YD");
}
}
    2)生产者:发送延迟消息
@Slf4j
@RestController
@RequestMapping("/ttl")
public void class SendMessageController{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 开始发消息
    @GetMapping("sendMsg/{message})
    public void SenfMsg(@PathVariable String message){
        rabbitTemplate.convertAndSend("X","XA","参数1为交换机,参数二为Routingkey,参数三为消息");
        rabbitTemplate.convertAndSend("X","XB","参数1为交换机,参数二为Routingkey,参数三为消息");
    }
}
    3)消费者:接收延迟消息
@Slf4j
@Component
public class DeadLetterQueueConsumer{
    // 接收消息
    @RabbitListener(queue="QD")
    public void receiveD(Message message, Channel channel){
        String msg = new String(message.getBody());
    }
}
(4)延迟队列的优化:上述案例中每增加一个延迟时间,就需要新增一个队列
@Configration
public class TtlQueueConfig{
    // 普通交换机名称
public static final String XExchange = "X";
// 死信交换机名称
public static final String YDeadExchange = "Y";
// 普通队列名称
public static final String cqueue= "QC";
// 死信队列名称
public static final String deadqueue = "QD";
// 声明 普通交换机
@Bean("XExchange")
public DirectExchange xExchange(){
    return new DirectExchange(XExchange);
}
// 声明 死信交换机
@Bean("YDeadExchange")
public DirectExchange xExchange(){
    return new DirectExchange(YDeadExchange);
}
// 声明 普通队列QC
@Bean("cqueue")
public Queue queueC(){
    Map<String,Object> argument = new HashMap<>();
    // 填写死信交换机
    argument.put("x-dead-letter-exchange",YDeadExchange);
    // 设置死信Routingkey
    argument.put("x-dead-letter-routing-key","YD");
    return QueueBuilder.durable(cqueue).withArguments(argument).build;
}
// 声明 死信队列QD
@Bean("dqueue")
public Queue queueD(){
    return QueueBuilder.durable(deadqueue).build();
}
@Bean
public Binding queueCBindingX(@Qualifier("cqueue") Queue queueC,
    @Qualifier("XExchange") Exchange exchange){
    return BindingBuilder.bind(queueC)to(exchange).with("XC");
}
@Bean
public Binding queueDBindingY(@Qualifier("dqueue") Queue queueD,
    @Qualifier("YDeadExchange") Exchange exchange){
    return BindingBuilder.bind(queueD)to(exchange).with("YD");
}
}
@Slf4j
@RestController
@RequestMapping("/ttl")
public void class SendMessageController{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 开始发消息
    @GetMapping("sendMsg/{message})
    public void SenfMsg(@PathVariable String message){
        rabbitTemplate.convertAndSend("X","XA","参数1为交换机,参数二为Routingkey,参数三为消息");
        rabbitTemplate.convertAndSend("X","XB","参数1为交换机,参数二为Routingkey,参数三为消息");
    }
    // 开始发消息  给消息设置过期时间
    @GetMapping("sendMsg/{message}/{ttlTime})
    public void SenfMsg(@PathVariable String message,@PathVariable String ttlTime){
        rabbitTemplate.convertAndSend("X","XA","参数1为交换机,参数二为Routingkey,参数三为消息");
        rabbitTemplate.convertAndSend("X","XB","参数1为交换机,参数二为Routingkey,参数三为消息");
        rabbitTemplate.convertAndSend("X","XC","1交换机2Routingkey3消息",msg -> {
            // 设置消息过期时间
            msg.getMessageProperties.setExpiration(ttlTime);
            return msg;
        });
    }
}
(5)延迟队列---基于死信队列存在的问题
    1) RabbitMQ只会检查第一个消息是否过期,如果过期则推入死信队列。如果第一个消息的延时很长,而第二个消息的延时很短,但是第二个消息并不会优先执行
    2)通过RabbitMQ插件实现延迟队列解决上述问题
    rabbitmq_delay_message_exchange
(6)总结
    RabbitMQ的延迟队列
    Java的DelayQueue
    Redis的Zset
    Quartz或者Kafka时间轮,这些方式各有特点,看需要使用的场景。
18、发布确认高级
(1)问题引出:生产环境中由于不明原因导致rabbitMQ重启,在重启期间生产者消息投递失败导致消息丢失,需要手动处理和恢复。所以如何能确保RabbitMQ的消息可靠投递呢?
(2)生产者向交换机发送消息未成功时, 将未成功发送的消息放入缓存,并启动定时任务对未成功发送的消息进行重新投递
(3)代码实现: 发布确认高级---回调接口RabbitMQTemplate.ConfirmCallback
配置文件新增配置:spring.rabbitmq.publisher-confirm-type=correlated
    1)NONE:默认模式,禁用发布确认模式
    2) correlated:发布成功后会进行回调
    3)SIMPLE:单个确认,一般不用
生产者
@Slf4j
@RestController
@RequestMapping("/ttl")
public void class SendMessageController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 开始发消息
    @GetMapping("sendMsg/{message})
    public void SenfMsg(@PathVariable String message){
        CorrelationData correlationData = new CorrelationData(id);
        rabbitTemplate.convertAndSend("X","XA","1为交换机,二为Routingkey,三为消息",correlationData);
    }
}
@Component
public class MyCallback implements RabbitMQTemplate.ConfirmCallback,RabbitMQTemplate.ReturnCallback{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 将实现类注册到rabbitTemplate的回调接口中
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }
    /**
    *  交换机回调确认方法
    *  correlationData:保存回调消息的ID及相关信息
    *  ack:交换机收到消息  true 收到   FALSE  未收到
    */ cause:null   /  失败原因
    @Override
    public void confirm(CorrelationData correlationData,boolean ack,String cause){
        if (!ack) {
            写入缓存
        }
    }
    // 只有在消息不可达目的地时才进行回退
    @Override
    public void returnMessage(Message message,int replyCode,String replyText,String exchange,String routingKey){
        
    }
}
19、发布确认高级---回退消息
(1) 在 仅开启了生产者确认机制 的情况下,交换机收到消息后会直接给消息生产者发送确认消息, 如果发现消息不可路由,那么消息会被直接丢弃,此时生产者是无法感知消息被丢失的事件的
(2) 通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者
(3)消息生产者代码
    新增配置信息:spring.rabbitmq.publisher-returns=true
20、备份交换机
(1)配置好备份交换机后,当交换机无法投递消息时,将消息发送给备份交换机。 还可以用独立消费者来进行检测和报警
配置类中声明交换机时需要指定备份交换机
// 声明 普通交换机
@Bean("XExchange")
public DirectExchange xExchange(){
    return ExchangeBuilder.directExchange(XExchange).durable(true).withArgyment("alternate-exchange","备份交换机".build();
}
(2)当备份交换机和消息回退同时启动时, 执行备份交换机优先级更高
21、RabbitMQ其他知识
(1)幂等性:同一操作发起一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用。也就是消息重复消费问题。
    1)MQ将消息发送给消费者,消费者在给MQ返回ack时网络中断,故MQ未收到确认消息,该条消息会重新发给其他消费者。或者网络重连后再次发给该消费者,导致消息被重复消费。
    2)解决思路: 使用全局ID或UUID或时间戳,每次消费时先用该ID判断是否被消费过
    3)消费端的幂等性保障:海量订单生成高峰期,生产端可能重复发了消息,就需要消费端实现幂等性。即使收到一样的消息也不会被消费多次。主流实现方案有如下两种:
    4)唯一ID+指纹码机制:主要是保证唯一性,判断数据库是否存在该id。但是高并发场景有性能问题,不是最推荐使用的。
    5)Redis原子性: 利用Redis执行setnx命令,天然具有幂等性。从而实现不重复消费
(2)优先级队列
    1)使用场景:订单催付功能,大客户的订单优先级相对较高。
    2)在控制界面可以添加
    3)代码实现:队列需要设置为优先级队列,消息需要设置消息的优先级。且需要将消息都放入到队列后再进行消费。
// 生产者
Map<String,Object> argument = new HashMap<>();
// 设置优先级 官方允许  0~255,不宜设置过大,浪费CPU内存
argument.put("x-max-priority",10);
for(int i=0;i<=10;i++){
    if(i==5){
        AMQP.BasicProperties  properties = new  AMQP.BasicProperties().build().priority(5).build();
        channel.basicPublish(normalExchange,"zhangsan",properties,message.getBytes());
    }else{
        channel.basicPublish(normalExchange,"zhangsan",null,message.getBytes());
    }
  }
}
    
(3)惰性队列: 消息保存在磁盘上,使用在消费者宕机的情况下
22、RabbitMQ集群
23、镜像队列--->保证集群中MQ队列在各个节点一致:在界面中添加策略
保证:就算集群中只剩下一台机器了,依然能消费队列里面的消息
24、实现高可用负载均衡:生产者动态连接MQ集群
(1)借助Haproxy+Keepalive实现负载均衡高可用
    

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值