RabbitMQ 学习笔记

RabbitMq

基础知识:

交换机常用类型,(headers 为头部参数模式,交换机和队列同时有相同的参数则匹配x=1,direct直连模式,fanout扇形模式(没有routing key),topic 主题模式),是否持久化,,anto delete 是否自动删除。

Rabbitmq使用场景(主要作用,解耦,异步,削峰):再根据具体业务讲解,把日志文件放到mq,再使用elk 去消费,做日志监控。分布式事务,下单,抢票,

过期时间TTL

过期时间ttl表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收,过了时间后会自动删除或者放到死信队列,可以对队列设置ttl,也可以对单独的消息设置ttl.

死信队列:

RabbitMq分布式事务(跨jvm级别的事务):

2PC     TCC     mq消息异步(Mq 事务消息)

实现要点:1、构建本地消息表及定时任务,扫描状态为未投递成功的消息,确保消息发送到mq,就可以更改消息状态为成功;2、RabbitMQ可靠消费;3、redis保证幂等


今天花费一整天的时间搞rabbitmq,记录一下:这里不讲搭建过程,只讲rabbitmq如何保证消息的发送,不丢失,和死信队列。

生产者如何保证消息被消费(高可靠,高可用):

保证生产者发送消息:

我们开启Mandatory, 触发回调函数,如果发送到了交换机ack为true,否则进行补偿,补偿我在消息发送的时候把 //msgId和message关系保存redis,用于补偿,补偿玩删除缓存。correlationData 里面保存了消息的唯一id

@ApiOperation("发送消息到消息队列 DirectExchange模式")
    @PostMapping("/send")
    public String send() {
        String msg = "hello";
        String msgId = UUID.randomUUID().toString();
        rabbitTemplate.convertAndSend("myDirectExchange", "my.direct.routing", msg,new CorrelationData(msgId)); // 指定 交换机和路由 发送到 myDirectQueue 队列上
        //msgId和message关系保存redis。用于补偿
        ArrayList<String> strings = new ArrayList<>();
        strings.add("myDirectExchange");
        strings.add("my.direct.routing");
        strings.add(msg);
        redisTemplate.opsForValue().set(msgId,strings,3, TimeUnit.MINUTES);
        return "success";
    }
 @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);

        // 开启Mandatory, 才能触发回调函数,无论消息推送结果如何都强制调用回调函数
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            // 当消息发送到交换机(exchange)时,该方法被调用.如果达到了交换机 ack=true,没有则为false
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("ConfirmCallback: "+"相关数据:" + correlationData);
                System.out.println("ConfirmCallback: "+"数据到交换机确认情况:" + ack);
                System.out.println("ConfirmCallback: "+"原因:" + cause);
                // 发到交换机失败
                if (!ack) {
                    // 从缓存中获取数据,进行补偿重试机制
                    ArrayList<String> msg = (ArrayList<String>) redisTemplate.opsForValue().get(correlationData.getId());
                    rabbitTemplate.convertAndSend(msg.get(0), msg.get(1), msg.get(2));
                    redisTemplate.delete(correlationData.getId());
                    System.out.println(msg);
                }
            }
        });

当消息从交换机到队列失败时,下面方法被调用。(若成功,则不调用),同样进行补偿

 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            // 当消息从交换机到队列失败时,该方法被调用。(若成功,则不调用)
            // 上面的confirm方法也会被调用,且ack = true
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("ReturnCallback: "+"消息:" + message);
                System.out.println("ReturnCallback: "+"回应码:" + replyCode);
                System.out.println("ReturnCallback: "+"回应信息:" + replyText);
                System.out.println("ReturnCallback: "+"交换机:" + exchange);
                System.out.println("ReturnCallback: "+"路由键:" + routingKey);
                // 补偿
                rabbitTemplate.convertAndSend(exchange, routingKey, message);
            }
        });

        return rabbitTemplate;
    }

保证消费者拿到消息:

开启手动确认ack,并开启confirm 和 return回调:

rabbitmq: #对于rabbitMQ的支持
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    # ------- 消息确认配置项 --------
    # 开启confirms 回调
    publisher-confirms: true
    # 开启return 回调
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual  # 手动确认消息是否被消费

如果消费了消息,我们会手动确认,删除消费的队列,如果出现了异常,我们放到死信队列(后面讲什么是死信队列)

@RabbitHandler
    @RabbitListener(queues = "myDirectQueue") // 队列
    public void process(Message msg, Channel channel) throws IOException {
        try{
            // 先幂等性判断 是否重复消费
//            int a = 1 / 0;  抛异常
            System.out.println("消费到消息"+msg);
            // 手动确认 删除队列消息
            channel.basicAck(msg.getMessageProperties().getDeliveryTag(), true);
        }catch (Exception e){
            log.error("消费消息失败了【】error:"+ msg);
            // 放到死信队列
            channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false,false);
            // 将消息重新放回队列  multiple=false 不从队列删除
//            channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,true);
        }

    }

死信队列:

通俗的说死信队列和普通队列一样,DLX, Dead-Letter-Exchange。利用DLX, 当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。消息变成死信一向有一下几种情况:

  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度

原理:我们首先会创建一个死信队列,

//死信交换机
    public static final String X_DEAD_LETTER_EXCHANGE = "x-dead-letter-exchange";
    //死信路由
    public static final String X_DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
    //死信队列
    public static final String DEAD_QUEUE = "dead-queue";

然后吧这个队列和普通队列进行关联,当普通队列出现死信对列时,就会转交到死信队列处理,(原队列还有其他业务要处理,一致重试下去不好),我们在去消费死信队列的消息就好了。

 //创建普通队列
    @Bean
    public Queue getNormalQueue(){
        Map args = new HashMap();
        //当消息发送异常的时候,消息需要路由到的交换机和routing-key,这里配的直接是发送至死信队列
        args.put("x-dead-letter-exchange",X_DEAD_LETTER_EXCHANGE);
        args.put("x-dead-letter-routing-key",X_DEAD_LETTER_ROUTING_KEY);
        //创建队列的时候,将死信绑定到队列中
        return QueueBuilder.durable(myDirectQueue).withArguments(args).build();
    }
//监听死信队列
    @RabbitListener(queues = {RabbitQueueAndExchange.DEAD_QUEUE})
    public void receiver(Message msg, Channel channel) throws IOException {
        // 手动确认 删除队列消息
        channel.basicAck(msg.getMessageProperties().getDeliveryTag(), true);
        System.out.println("dead queue 收到消息>>>>>>>>>"+msg);
    }

对比3附图,发现了dead-queue 和 myDirectQueue 的关系,当出现了异常,myDirectQueue没有消息,消息丢到了dead-queue 队列里。

 

以上是第一种方式:

还有种方式,在发送消息时,把消息的信息发送到一张日志表里面,并初始化状态未消费,然后在、setConfirmCallback里面,如果发送成功修改状态为成功,在定义个计数器去扫描日志表,扫描未消费的重试几次。

Employee emp = employeeMapper.getEmployeeById(employee.getId());
            //生成消息的唯一id
            String msgId = UUID.randomUUID().toString();
            MailSendLog mailSendLog = new MailSendLog();
            mailSendLog.setMsgId(msgId);
            mailSendLog.setCreateTime(new Date());
            mailSendLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
            mailSendLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
            mailSendLog.setEmpId(emp.getId());
            mailSendLog.setTryTime(new Date(System.currentTimeMillis() + 1000 * 60 * MailConstants.MSG_TIMEOUT));
            mailSendLogService.insert(mailSendLog);
            rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(msgId));
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
        rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
            String msgId = data.getId();
            if (ack) {
                logger.info(msgId + ":消息发送成功");
                mailSendLogService.updateMailSendLogStatus(msgId, 1);//修改数据库中的记录,消息投递成功
            } else {
                logger.info(msgId + ":消息发送失败");
            }
        });
        rabbitTemplate.setReturnCallback((msg, repCode, repText, exchange, routingkey) -> {
            logger.info("消息发送失败");
        });
        return rabbitTemplate;

定时器重试 :

@Scheduled(cron = "0/10 * * * * ?")
    public void mailResendTask() {
        List<MailSendLog> logs = mailSendLogService.getMailSendLogsByStatus();
        if (logs == null || logs.size() == 0) {
            return;
        }
        logs.forEach(mailSendLog->{
            if (mailSendLog.getCount() >= 3) {
                mailSendLogService.updateMailSendLogStatus(mailSendLog.getMsgId(), 2);//直接设置该条消息发送失败
            }else{
                mailSendLogService.updateCount(mailSendLog.getMsgId(), new Date());
                Employee emp = employeeService.getEmployeeById(mailSendLog.getEmpId());
                rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(mailSendLog.getMsgId()));
            }
        });
    }

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值