死信消息与延时消息的两种实现

死信队列介绍

  • 死信队列:DLX,dead-letter-exchange
  • 利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX

Dead Letter Exchanges(DLX)特性

RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送

消息变成死信有以下几种情况

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

死信队列 听上去像 消息“死”了 其实也有点这个意思,死信队列 是 当消息在一个队列 因为下列原因:

      消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
     消息超期 (rabbitmq Time-To-Live -> messageProperties.setExpiration())
     队列超载
变成了 “死信” 后 被重新投递(publish)到另一个Exchange 该Exchange 就是DLX 然后该Exchange 根据绑定规则 转发到对应的 队列上 监听该队列 就可以重新消费 说白了 就是 没有被消费的消息 换个地方重新被消费。

å¨è¿éæå¥å¾çæè¿°

死信队列实现

使用springboot整合rabbitMQ实现一个死信队列。

  • 引入依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>
</dependencies>
  • rabbitMQ的配置MQConfig.java

创建队列  fanout_queue fanout 类型的交换机  fanoutExchange ,绑定 fanout_queue 与 fanoutExchange。

创建死信队列  dead_queue Direct 类型的交换机  dead_exchange,绑定 dead_queue与 dead_exchange,需要设置路由为 dead_routing_key。

在创建 fanout_queue 队列时,知道消息死信之后路由的交换机。

x-dead-letter-exchange 设置指向 dead_exchange
x-dead-letter-routing-key 设置路由 dead_routing_key

@Configuration
public class MQConfig {
    // ---------------------- 死信消息 -------------------------
    /**
     * 定义死信队列相关信息
     */
    // 消息死信存放的队列
    public final static String deadQueueName = "dead_queue";
    public final static String deadRoutingKey = "dead_routing_key";
    public final static String deadExchangeName = "dead_exchange";
    /**
     * 死信队列 交换机标识符
     */
    public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
    /**
     * 死信队列交换机绑定键标识符
     */
    public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";

    // 队列fanout_queue
    public static final String FANOUT_QUEUE = "fanout_queue";

    // fanout 交换机
    public static final String FANOUT_EXCHANGE_NAME = "fanoutExchange";


    /**
     * 队列,消息死信后,投递到deadExchangeName
     */
    @Bean("fanoutQueue")
    Queue fanoutQueue(){
        // 将普通队列绑定到死信队列交换机上
        Map<String, Object> args = new HashMap<>(2);
        args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
        args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
        Queue queue = new Queue(FANOUT_QUEUE, true, false, false, args);
        return queue;
    }
    /**
     * 交换机
     */
    @Bean("fanoutExchange")
    FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE_NAME);
    }

    /**
     * 绑定交换机fanoutExchange与队列fanoutQueue
     * @param fanoutQueue 创建的队列fanoutQueue
     * @param fanoutExchange 创建的交换机fanoutExchange
     * @return
     */
    @Bean
    Binding bindingExchange(Queue fanoutQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
    }



    /**
     * 消息死信后存放的队列
     * @return
     */
    @Bean("deadQueue")
    Queue deadQueue(){
        Queue queue = new Queue(deadQueueName, true);
        return queue;
    }

    /**
     * 创建死信交换机
     */
    @Bean("deadExchange")
    public DirectExchange deadExchange() {
        return new DirectExchange(deadExchangeName);
    }

    /**
     * 死信队列与死信交换机绑定,指定路由key
     */
    @Bean
    public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadRoutingKey);
    }
    // ---------------------- 死信消息 -------------------------
}
  • 写一个接口往队列fanout_queue 中发送消息,

设置消息发送到交换机 异常回调函数 ConfirmCallback

设置消息成功发送到交换机后,没有成功分配到队列的 异常回调函数 ReturnCallback

@RequestMapping(value = "/deadSendMessage")
    @ResponseBody
    public String send() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("llllllll", "xx@163.com");
        jsonObject.put("timestamp", 0);
        String jsonString = jsonObject.toJSONString();
        System.out.println("jsonString:" + jsonString);

        CorrelationData cData = new CorrelationData("email-" + new Date().getTime());
        rabbitTemplate.setConfirmCallback((correlationData,isack,cause) -> {
            System.out.println("本次消息的唯一标识是:" + correlationData);
            System.out.println("是否存在消息拒绝接收?" + isack);
            if(isack == false){
                System.out.println("消息拒绝接收的原因是:" + cause);
            }else{
                System.out.println("消息发送成功");
            }
        });

        rabbitTemplate.setReturnCallback( (message,i,s,s1,s2) -> {
            System.out.println("err code :" + i);
            System.out.println("错误消息的描述 :" + s);
            System.out.println("错误的交换机是 :" + s1);
            System.out.println("错误的路右键是 :" + s2);
        });
        rabbitTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE_NAME,"",jsonString,cData);


        return "success";
    }
  • 监听两个消息队列观察效果

在正常的监听队列中,

利用int result  = 1/timestamp; // 制造异常,发拒绝消息,

拒绝消息后,就造成了死信消息,然后就会发现,在死信队列中,消费了那条死信消息。

@Component
public class Consumer {

    @RabbitListener(queues = "fanout_queue")
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
        // 获取消息Id
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        System.out.println("消费者获取生产者消息msg:"+msg+",消息id"+messageId);
        JSONObject jsonObject = null;
        try{
            jsonObject = JSONObject.parseObject(msg);
        }catch (Exception e){
            // // 手动ack
            Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
            // 手动签收
            channel.basicAck(deliveryTag, false);
            return;
        }
        Integer timestamp = jsonObject.getInteger("timestamp");

        try {
            int result  = 1/timestamp; // 制造异常,出发拒绝消息
            System.out.println("result"+result);
            // // 手动ack
            Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
            // 手动签收
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            //拒绝消费消息(丢失消息) 给死信队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }

        System.out.println("执行结束....");
    }



    @RabbitListener(queues = "dead_queue")
    public void process2(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("消费者获取生产者消息接收时间:"+ simpleDateFormat.format(new Date()));

        // 获取消息Id
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        System.out.println("死信消费者获取生产者消息msg:"+msg+",消息id"+messageId);
        // // 手动ack
        Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        // 手动签收
        channel.basicAck(deliveryTag, false);

        System.out.println("执行结束....");
    }

}

 

延时消息实现

方式一:Time To Live(TTL) 特性

Time To Live(TTL) 特性

RabbitMQ可以针对Queue设置x-expires 或者 针对Message设置 x-message-ttl,来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
RabbitMQ针对队列中的消息过期时间有两种方法可以设置。
A: 通过队列属性设置,队列中所有消息都有相同的过期时间。
B: 对消息进行单独设置,每条消息TTL可以不同。

如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter

消息过期之后就变成私信消息了。

  • 配置:
    // ---------------------- 使用死信消息特性,延时信息 -------------------------
    /**
     * 队列,消息死信后,投递到deadExchangeName
     */
    @Bean("fanoutTimeQueue")
    Queue fanoutTimeQueue(){
        // 将普通队列绑定到死信队列交换机上
        Map<String, Object> args = new HashMap<>(2);
        args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
        args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
        Queue queue = new Queue("fanoutTimeQueue", true, false, false, args);
        return queue;
    }
    /**
     * 交换机
     */
    @Bean("fanoutTimeExchange")
    FanoutExchange fanoutTimeExchange() {
        return new FanoutExchange("fanoutTimeExchange");
    }

    /**
     * 绑定交换机fanoutTimeExchange与队列fanoutTimeQueue
     * @param fanoutTimeQueue 创建的队列
     * @param fanoutTimeExchange 创建的交换机
     * @return
     */
    @Bean
    Binding bindingTimeExchange(Queue fanoutTimeQueue, FanoutExchange fanoutTimeExchange) {
        return BindingBuilder.bind(fanoutTimeQueue).to(fanoutTimeExchange);
    }
    // ---------------------- 使用死信消息特性,延时信息 -------------------------

在MQConfig配置类中,在配置一个正常的队列fanoutTimeQueue 和 交换机 fanoutTimeExchange,绑定两个。

并且队列 fanoutTimeQueue 指定路 私信路由 和 死信交换机参数,指向了前面定义的死信队列。

 在发送消息的适合,设置消息的生存时间。时间一到,超时,就形成私信消息。

  • 发送消息:
 @RequestMapping(value = "/sendTimeMessage")
    @ResponseBody
    public String sendTimeMessage() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("email", "xx@163.com");
        jsonObject.put("timestamp", 0);
        String jsonString = jsonObject.toJSONString();
        System.out.println("jsonString:" + jsonString);

        CorrelationData cData = new CorrelationData("email-" + new Date().getTime());

        rabbitTemplate.setConfirmCallback((correlationData,isack,cause) -> {
            System.out.println("本次消息的唯一标识是:" + correlationData);
            System.out.println("是否存在消息拒绝接收?" + isack);
            if(isack == false){
                System.out.println("消息拒绝接收的原因是:" + cause);
            }else{
                System.out.println("消息发送成功");
            }
        });

        rabbitTemplate.setReturnCallback( (message,i,s,s1,s2) -> {
            System.out.println("err code :" + i);
            System.out.println("错误消息的描述 :" + s);
            System.out.println("错误的交换机是 :" + s1);
            System.out.println("错误的路右键是 :" + s2);
        });


        // 声明消息处理器 这个对消息进行处理 可以设置一些参数 对消息进行一些定制化处理 我们这里 来设置消息的编码 以及消息的过期时间
        // 因为在.net 以及其他版本过期时间不一致 这里的时间毫秒值 为字符串
        MessagePostProcessor messagePostProcessor = message -> {
            MessageProperties messageProperties = message.getMessageProperties();
            // 设置编码
            messageProperties.setContentEncoding("utf-8");
            // 设置过期时间10*1000毫秒
            messageProperties.setExpiration("10000");
            return message;
        };
        // fanoutTimeExchange 发送消息 10*1000毫秒后过期 形成死信,具体的时间可以根据自己的业务指定
        rabbitTemplate.convertAndSend("fanoutTimeExchange", "", jsonString, messagePostProcessor, cData);

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(simpleDateFormat.format(new Date()));
        return "success";
    }

总结上面的方式:经过测试,我们可以发现,当我们先增加一条过期时间大(10000)的A消息进入,之后再增加一个过期时间小的(1000)消息B,并没有出现想象中的B消息先被消费,A消息后被消费,而是出现了当10000过去的时候,AB消息同时被消费,也就是B消息的消费被阻塞了。

为什么会出现这样的现象呢?
我们知道利用TTL DLX特性实现的方式,实际上在第一个延时队列C里面设置了dlx,生产者生产了一条带ttl的消息放入了延时队列C中,等到延时时间到了,延时队列C中的消息变成了死信,根据延时队列C中设置的dlx的exchange的转发规则,转发到了实际消费队列D中,当该队列中的监听器监听到消息时就会正式开始消费。那么实际上延时队列中的消息也是放入队列中的,队列满足先进先出,而延时大的消息A还没出队,所以B消息也不能顺利出队。

方式二:利用Rabbitmq的插件x-delay-message实现

为了解决上面的问题,Rabbitmq实现了一个插件x-delay-message来实现延时队列。

这里介绍使用插件的方式,不过需要rabbitmq要是3.6版本以上,也就是说,加入你的rabbitmq版本太老只能用TTL。

  • x-delay-message安装

插件的作用:基于插件存放消息在延时交换机里(x-delayed-message exchange)。

①:生产者将消息(msg)和路由键(routekey)发送指定的延时交换机(exchange)上
②:延时交换机(exchange)存储消息等待消息到期根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它
③:队列(queue)再把消息发送给监听它的消费者(customer)

插件没有用到死信消息的特性。

安装:

先去官网下载插件
在这里插入图片描述
下载的插件放到rabbitmq的plugins里,执行命令安装插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

  • 延时消息的实现:
  • MQConfig.java 
@Configuration
public class MQConfig {
    // --------------------- 使用插件实现延时队列 --------------------------
    /**
     * 延时队列交换机
     * 注意这里的交换机类型:CustomExchange
     * @return CustomExchange自定义交换机,可以自己定义设置参数
     */
    @Bean("cfgUserDelayExchange")
    public CustomExchange delayExchange(){
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange("delay_exchange","x-delayed-message",true, false,args);
    }

    /**
     * 延时队列
     * @return
     */
    @Bean("cfgDelayQueue")
    public Queue delayQueue(){
        return new Queue("delay_queue",true);
    }

    /**
     * 给延时队列绑定交换机
     * @return
     */
    @Bean
    public Binding cfgDelayBinding(Queue cfgDelayQueue,CustomExchange cfgUserDelayExchange){
        return BindingBuilder.bind(cfgDelayQueue).to(cfgUserDelayExchange).with("delay_key").noargs();
    }
    // --------------------- 使用插件实现延时队列 --------------------------
}

配置创建了一个队列cfgDelayQueue。创建了一个自定义的交换机cfgUserDelayExchange,自定义的交换机设置参数

Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange("delay_exchange","x-delayed-message",true, false,args);
  • 发送消息
@RequestMapping(value = "/sendTimeMessageByPlug")
    @ResponseBody
    public String sendTimeMessageByPlug() {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        //这里的消息可以是任意对象,无需额外配置,直接传即可
        System.out.println("===============延时队列生产消息====================");
        System.out.println(String.format("发送时间:{},发送内容:{}", LocalDateTime.now(), list.toString()));


        rabbitTemplate.setConfirmCallback((correlationData,isack,cause) -> {
            System.out.println("本次消息的唯一标识是:" + correlationData);
            System.out.println("是否存在消息拒绝接收?" + isack);
            if(isack == false){
                System.out.println("消息拒绝接收的原因是:" + cause);
            }else{
                System.out.println("消息发送成功");
            }
        });

        rabbitTemplate.setReturnCallback( (message,i,s,s1,s2) -> {
            System.out.println("err code :" + i);
            System.out.println("错误消息的描述 :" + s);
            System.out.println("错误的交换机是 :" + s1);
            System.out.println("错误的路右键是 :" + s2);
        });
        this.rabbitTemplate.convertAndSend(
                "delay_exchange",
                "delay_key",
                list,
                message -> {
                    //注意这里时间可以使long,而且是设置header
                    message.getMessageProperties().setHeader("x-delay",10000);
                    return message;
                }
        );
        return "success";
    }

注意,这里发送消息的时候,需要声明消息处理器MessagePostProcessor,设置header参数x-delay,延时的时间。

message -> {
    //注意这里时间可以使long,而且是设置header
    message.getMessageProperties().setHeader("x-delay",10000);
    return message;
}
  • ,监听队列
/**
     * 默认情况下,如果没有配置手动ACK, 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK
     * 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完
     * 解决方案:手动ACK,或者try-catch 然后在 catch 里面将错误的消息转移到其它的系列中去
     * spring.rabbitmq.listener.simple.acknowledge-mode = manual
     * @param list 监听的内容
     */
    @RabbitListener(queues = "delay_queue")
    public void cfgUserReceiveDealy(List<Integer> list, Message message, Channel channel,@Headers Map<String, Object> headers) throws IOException {
        System.out.println("===============接收队列接收消息====================");
        System.out.println(String.format("接收时间:{},接受内容:{}", LocalDateTime.now(), list.toString()));
        //通知 MQ 消息已被接收,可以ACK(从队列中删除)了
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        try {
            System.out.println("result"+list.toString());
            // // 手动ack
            Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
            // 手动签收 false 表示从队列中删除,true表示重新放回队列
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            System.out.println("============消费失败,尝试消息补发再次消费!==============");
            System.out.println(e.getMessage());
            /**
             * basicRecover方法是进行补发操作,
             * 其中的参数如果为true是把消息退回到queue但是有可能被其它的consumer(集群)接收到,
             * 设置为false是只补发给当前的consumer
             */
            channel.basicRecover(false);
        }
    }

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值