RabbitMQ 高级特性

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景;

RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式;

  • confirm 确认模式
  • return 退回模式

对于确认模式:

  • 使用 rabbitTemplate.**setConfirmCallback **设置回调函数;当消息发送到exchange后回调confirm方法;在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理;

对于退回模式

  • 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,则会将消息退回给producer,并执行回调函数returnedMessage;

请添加图片描述

rabbitmq 整个消息投递的路径为:

请添加图片描述

  • 消息从 product 到 exchange 是否成功可以通过 confirmCallback 来进行确认;
  • 消息从 exchange 到 queue路由是否成功可以通过 **returnCallback ** 来进行确认;

我们可以利用这两个 callback 控制消息的可靠性投递;

生产者确认 confirm & return

confirm确认模式

Maven创建 rabbitmq-confirm model

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
</dependencies>

rabbitmq连接信息

server:
  port: 8086

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #开启confirm确认

声明队列、交换机以及绑定队列和交换机

package com.example.config;


import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * explain:消息确认 队列需要持久化 交换机需要持久化
 *
 * @author Hope
 * @date 2022/6/14
 * @see RabbitMQConfig
 */
@Configuration
public class RabbitMQConfig {

    /**
     * 声明order.A队列
     * 第一个参数:队列名称
     * 第二个参数:队列是否持久化
     * 第三个参数:是否为排外队列,如果排外队列则只能一个消费者进行访问消费
     * 第四个参数:是否自动删除
     *
     * @author Hope
     */
    @Bean
    public Queue orderQueue() {
        return new Queue("order.A", true, false, false);
    }

    /**
     * 声明交换机
     * 第二个参数:队列是否持久化
     * 第三个参数:是否自动删除
     *
     * @author Hope
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("order_direct_ex", true, false);
    }

    /**
     * 将队列与交换机绑定
     *
     * @param orderQueue
     * @param directExchange
     * @author Hope
     */
    @Bean
    public Binding binding(Queue orderQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(orderQueue).to(directExchange).with("order");
    }
}

发送消息

package com.example.controller;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * explain:生产者确认测试 Controller
 *
 * @author Hope
 * @date 2022/6/14
 * @see ConfirmController
 */
@RestController
public class ConfirmController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsgConfirm")
    public void sendMsg() {
        rabbitTemplate.convertAndSend("order_direct_ex", "order", "生产者消息");
    }
}

编写confirm回调方法

package com.example.confirm;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
@Slf4j
public class RabbitmqConfirm implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /*
        @PostConstruct在 @Autowired 后执行
     */
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
    }

    /*
        confirm 确认
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息进入交换机成功");
        } else {
            log.info("消息进入交换机失败, cause: {}", cause);
        }
    }
}

生产者最终效果如下

请添加图片描述

消费者消费消息, 消费者这里就是mq一个基础的消费端, 只需要监听队列即可, 这里直接使用上一篇博客的消费者,

效果如下
请添加图片描述

return 退回模式

修改生产者配置文件,开启回退模式

server:
  port: 8086

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #开启confirm确认
    publisher-returns: true #开启return确认

编写return回调方法

package com.example.confirm;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * explain:生产者消息确认监控
 * confirm监控范围: 生产者->交换机
 * returned监控范围: 交换机->队列
 *
 * @author Hope
 * @date 2022/6/14
 * @see RabbitmqConfirm
 */
@Component
@Slf4j
public class RabbitmqConfirm implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /*
        @PostConstruct在 @Autowired 后执行
     */
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    /*
        confirm 确认
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息进入交换机成功");
        } else {
            log.info("消息进入交换机失败, cause: {}", cause);
        }
    }

    /*
       return 退回确认
    */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息从交换机路由至队列失败, replyCode: {}, replyText: {}, exchange: {}, routingKey: {}", replyCode, replyText, exchange, routingKey);
    }
}

测试: 启动生产者和消费者, 修改controller中的路由key, 让消息无法发送到队列, 观察控制台打印;

小节:

  1. 对于确认模式和回退模式都是针对于生产者确认;

  2. 生产者确认开启步骤: 配置文件开启confirm和return, 编写监控类实现确认和回退的回调函数, 交换机要声明持久化否则确认模式将毫无意义;

消费者确认(ACK)

ack指 Acknowledge,确认; 表示消费端收到消息后的确认方式;

有三种确认方式:

• 自动确认:acknowledge=“none”

• 手动确认:acknowledge=“manual”

• 根据异常情况确认:acknowledge=“auto”

如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息;

代码实现:

在配置文件中开启手动ACK, 配置最好在生产者和消费者都配置一份, ACK是必需要配置在消费者中;

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #开启confirm确认
    publisher-returns: true #开启return确认
    listener:
      simple:
        acknowledge-mode: manual #ACK

消费者代码

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
@RabbitListener(queues = "hello_world") //监听hello_world队列
@RabbitListener(queues = "fanout.A")
@RabbitListener(queues = "direct.A")
@RabbitListener(queues = "topic.A")
@RabbitListener(queues = "order.A")
public class ConsumerService {

    @RabbitHandler
    public void receive(String message, Channel channel,
                        @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        try {
            //deliveryTag 信道中每接收一条消息自增1,
            log.info("message: {}, deliveryTag: {}", message, deliveryTag);
            if (message.contains("苹果")) {
                throw new RuntimeException("不许购买苹果手机");
            }
             /*
                deliveryTag 信道中每接收一条消息自增1,
                第二个参数代表是否批量确认,我们这里只确认一条消息所以设置为false
                basicAck 代表接收消息后业务逻辑处理正常,rabbitmq可以将此消息删除掉了
             */
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            log.info("消费失败,重回队列, message: {}, deliveryTag: {}", message, deliveryTag);
            try {
                     /*
                         deliveryTag 每次接收消息+1,
                        第二个参数代表是否批量确认,我们这里只确认一条消息所以设置为false
                        第三个参数代表是否要将该消息重回队列
                    */
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException e1) {
                log.error("重回队列异常: {}", e1);
            }
        }
    }

}

但是上面的方法存在一个问题, 如果消息确认失败, 失败消息不断的重新回到队列中,如果有大量的失败消息将造成队列的大量消息积压和资源的占用;

对于消费失败的消息我们不能让他无限的重新回到队列中去,mq消息中用一个布尔类型的值来标记消息是否已经重回过队列,那么我们在消费的时候增加一个消息是否已经重新回到过队列的判断就可以解决这个问题了;

代码实现:

/**
     * 消费者确认ACK
     *
     * @param message
     * @param channel
     * @param deliveryTag 信道中每接收一条消息自增1
     * @param redelivered true-代表已经重回过队列
     * @author Hope
     */
    @RabbitHandler
    public void receive(String message, Channel channel,
                        @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                        @Header(AmqpHeaders.REDELIVERED) boolean redelivered) {
        try {
            // Thread.sleep(3000);
            log.info("message: {}, deliveryTag: {}, redelivered: {}", message, deliveryTag, redelivered);
            if (message.contains("苹果")) {
                throw new RuntimeException("不许购买苹果手机");
            }
             /*
                basicAck 手动确认
                deliveryTag 信道中每接收一条消息自增1
                第二个参数代表是否批量确认,我们这里只确认一条消息所以设置为false
             */
            channel.basicAck(deliveryTag, false);// 确认消息
        } catch (Exception e) {
            if (redelivered) {
                log.info("第二次消费失败,不允许重回队列, " +
                        "message: {}, deliveryTag: {}, redelivered: {}", message, deliveryTag, redelivered);

                try {
                     /*
                        basicAck 手动确认
                        deliveryTag 信道中每接收一条消息自增1
                        第二个参数代表是否批量确认,我们这里只确认一条消息所以设置为false
                        第三个参数代表是否要将该消息重回队列
                    */
                    channel.basicNack(deliveryTag, false, false);
                } catch (IOException e1) {
                    log.error("拒绝重回队列异常: {}", e1);
                }
            } else {
                log.info("消费失败,重回队列 message: {}, deliveryTag: {}, redelivered: {}"
                        , message, deliveryTag, redelivered);
                try {
                    channel.basicNack(deliveryTag, false, true);
                } catch (IOException e1) {
                    log.error("重回队列异常: {}", e1);
                }
            }
        }
    }

消费端限流

请添加图片描述

如上图所示:

假设 Consumer需要停机维护, 生产者发送5000条消息到MQ中, 此时Consumer停机消息积压在MQ中, Consumer最大处理消息数量2条, 当Consumer系统成功启动后,消费者会一次性将MQ的消息全部拉取到自己的服务,导致服务在短时间内会处理大量的业务,可能会导致系统服务的崩溃;

可以通过MQ中的 listener-container 配置属性 perfetch = 2,

表示消费端每次从mq拉去2条消息来消费,直到手动确认消费完毕后,才会继续拉去下一条消息;

  • 在rabbit:listener-container 中配置 prefetch属性设置消费端一次拉取多少消息
  • 消费端的确认模式一定为手动确认;acknowledge=“manual”

代码实现:

消费者配置开启限流, 每次读取两条消息

server:
  port: 8082

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #开启confirm确认
    publisher-returns: true #开启return确认
    listener:
      simple:
        acknowledge-mode: manual #ACK
        prefetch: 2 #消费者在未确认消息时一次拉取消息的数量

测试

请添加图片描述

TTL过期时间

TTL 全称 Time To Live(过期时间);当消息到达存活时间后,还没有被消费,会被自动清除;

RabbitMQ可以对消息设置过期时间,也可以对整个队列设置过期时间;

  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期;
  • 由于队列是先进先出的,所以如果设置单个消息的过期时间并没有实际意义;
  • 例如:设置消息A的过期时间为10秒,消息B的过期时间为5秒,但是先将消息A发送至队列,那么只有等消息A被消费或者到期移除后才会将消息B消费或者到期移除;

请添加图片描述

代码实现:

我们直接修改之前的生产者代码, 在声明队列是添加过期时间;


@Configuration
public class RabbitMQConfig {

    /**
     * 声明order.A队列
     * 第一个参数:队列名称
     * 第二个参数:队列是否持久化
     * 第三个参数:是否为排外队列,如果排外队列则只能一个消费者进行访问消费
     * 第四个参数:是否自动删除
     * 第五个参数: 队列过期时间, 单位ms ,key = x-message-ttl
     *
     * @author Hope
     */
    @Bean
    public Queue orderQueue() {
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-message-ttl", 5000); //声明过期队列,设置过期时间为5秒

        return new Queue("order.A", true, false, false, arguments);
    }

    /**
     * 声明交换机
     * 第二个参数:队列是否持久化
     * 第三个参数:是否自动删除
     *
     * @author Hope
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("order_direct_ex", true, false);
    }

    /**
     * 将队列与交换机绑定
     *
     * @param orderQueue
     * @param directExchange
     * @author Hope
     */
    @Bean
    public Binding binding(Queue orderQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(orderQueue).to(directExchange).with("order");
    }
}

我们不启动消费者, 直接启动生产者, 观察rabbitmq控制台5秒后消息是否会自动移除;

这里遇到一个问题, 一旦创建了队列和交换机,就不能修改其标志了;例如,如果创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重新创建;

队列一旦创建 Features(特性)不可改变, order.A队列之前已经创建过了, 所以再添加过期时间是会报错的;

死信队列

死信队列:当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是Dead Letter Exchange(死信交换机 简写:DLX);

  1. 死信交换机和死信队列和普通的没有区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  3. 消息成为死信的三种情况:
    • 队列消息长度到达限制;
    • 消费者拒接消费消息,并且不重回队列;
    • 原队列存在消息过期设置,消息到达超时时间未被消费;

请添加图片描述

设置死信队列绑定死信交换机:

给队列设置参数: x-dead-letter-exchangex-dead-letter-routing-key

代码实现:

初始化死信队列和死信交换机并且绑定

 /**
     * 声明死信队列
     * 第一个参数:队列名称
     * 第二个参数:队列是否持久化
     * 第三个参数:是否为排外队列,如果排外队列则只能一个消费者进行访问消费
     * 第四个参数:是否自动删除
     *
     * @author Hope
     */
    @Bean
    public Queue delQueue() {
        return new Queue("order_del_queue", true, false, false);
    }

    /**
     * 声明死信交换机
     * 第二个参数:队列是否持久化
     * 第三个参数:是否自动删除
     *
     * @author Hope
     */
    @Bean
    public DirectExchange delExchange() {
        return new DirectExchange("order_del_ex", true, false);
    }

    /**
     * 将队列与交换机绑定
     *
     * @author Hope
     */
    @Bean
    public Binding bindingDel(Queue delQueue, DirectExchange delExchange) {
        return BindingBuilder.bind(delQueue).to(delExchange).with("order_del");
    }

给order.A绑定死信队列

需要注意的是队列一旦声明就不可以变更了,所以这里记得在管控台删除一下队列;

    /**
     * 声明order.A队列
     * 第一个参数:队列名称
     * 第二个参数:队列是否持久化
     * 第三个参数:是否为排外队列,如果排外队列则只能一个消费者进行访问消费
     * 第四个参数:是否自动删除
     * 第五个参数: 声明过期队列, 设置死信队列, 设置死信交换机
     *
     * @author Hope
     */
    @Bean
    public Queue orderQueue() {
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-message-ttl", 5000); //声明过期队列,设置过期时间为5秒
        arguments.put("x-dead-letter-routing-key", "order_del"); //设置死信队列
        arguments.put("x-dead-letter-exchange", "order_del_ex"); //设置死信交换机

        return new Queue("order.A", true, false, false, arguments);
    }

测试order.A的过期消息是否会路由至死信队列

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费;

提出需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存;
  2. 新用户注册成功7天后,发送短信问好;

实现方式:

  1. 定时器

  2. 延迟队列

  3. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费;

  4. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果;

请添加图片描述

注意:在RabbitMQ中并未提供延迟队列功能;

但是可以使用:TTL+死信队列 组合实现延迟队列的效果;

请添加图片描述

保证幂等性

幂等性例子:比如你购物只想购买一件商品,但是网络卡顿,你按了多次提交按钮后,系统将此订单生成了两次, 如上即数据库生成了两条订单记录, 即产生了幂等性的问题;

**正确的情况:**一个商品页点提交,不管如何, 只会产生一条订单信息;

解决方案: 全局唯一ID(把订单ID添加到redis, 新订单查询redis中有没有重复ID), 数据库主键 唯一索引

redis解决方案:

思路:

  1. 消费者要开启手动ACK模式, 手动确认消息;
  2. 首先消息要有全局唯一的 id;
  3. 消费者 消费时用唯一id去redis中去查;
  4. redis中没有代表消息没有被消费过, 消费后将唯一id, 存入Redis中;
  5. redis中有代表消息重复, 丢弃消息;

生产者代码实现

创建rabbitmq-idempotent-p项目

Maven依赖:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--JSON 数据转换支持-->
        <!--alibaba fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

配置文件

server:
  port: 8087

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #开启confirm确认
    publisher-returns: true #开启return确认
    listener:
      simple:
        acknowledge-mode: manual #ACK
 

添加rabbitmq配置


@Configuration
public class RabbitMQConfig {


    /**
     * 声明order.A队列
     * 第一个参数:队列名称
     * 第二个参数:队列是否持久化
     * 第三个参数:是否为排外队列,如果排外队列则只能一个消费者进行访问消费
     * 第四个参数:是否自动删除
     * 第五个参数: 声明过期队列, 设置死信队列, 设置死信交换机
     *
     * @author Hope
     */
    @Bean
    public Queue orderQueue() {
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-message-ttl", 50000); //声明过期队列,设置过期时间为50秒

        return new Queue("object.A", true, false, false, arguments);
    }

    /**
     * 声明交换机
     * 第二个参数:队列是否持久化
     * 第三个参数:是否自动删除
     *
     * @author Hope
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("order_direct_ex", true, false);
    }

    /**
     * 将队列与交换机绑定
     *
     * @param orderQueue
     * @param directExchange
     * @author Hope
     */
    @Bean
    public Binding binding(Queue orderQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(orderQueue).to(directExchange).with("order");
    }

}

模拟实体


@Data
@AllArgsConstructor
@NoArgsConstructor
public class MqModel implements Serializable {
    private String id;
    private String mes;
}

序列化工具


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

/**
 * explain:Json工具类
 *
 * @author Hope
 * @date 2022/3/22
 * @see com.alibaba.fastjson
 */
public class JacksonJsonUtil {

    /**
     * 对象转json
     *
     * @param obj 任意VO(页面对象) 集合
     * @return java.lang.String
     * @author Hope
     */
    public static String objToJson(Object obj) {
        String str;
        if (obj == null) {
            return null;
        }
        JSON.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
        str = JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat);
        return str;

    }

    /**
     * json转对象
     *
     * @param jsonStr   json形式的对象
     * @param classType 对象类型
     * @return T
     * @author Hope
     */
    public static <T> T jsonToObj(String jsonStr, Class<T> classType) {
        if (jsonStr == null) {
            return null;
        }
        JSON.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
        return JSON.parseObject(jsonStr, classType);
    }

}

发送消息


@RestController
public class PIdempotentController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsgConfirm")
    public void sendMsg() {
        MqModel mqModel = new MqModel();
        // 假设此id 是消息的全局唯一id
        mqModel.setId("12000");
        mqModel.setMes("生产者消息");

        //序列化对象
        String obj = JacksonJsonUtil.objToJson(mqModel);

        rabbitTemplate.convertAndSend("order_direct_ex", "order", obj);

    }
}

生产者最终效果

请添加图片描述

消费者代码实现

创建 rabbitmq-idempotent-c 项目

Maven依赖:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--JSON 数据转换支持-->
        <!--alibaba fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!--RedisTemplate 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

配置文件

server:
  port: 8088

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #开启confirm确认
    publisher-returns: true #开启return确认
    listener:
      simple:
        acknowledge-mode: manual #ACK
  jmx:
    enabled: false
  redis:
    ## Redis数据库索引(默认为0)
    database: 0
    ## Redis服务器地址
    host: 127.0.0.1
    ## Redis服务器连接端口
    port: 6379
    ## Redis服务器连接密码(默认为空)
    password:

    lettuce:
      pool:
        ## 连接池最大连接数(使用负值表示没有限制)
        max-active: 60
        ## 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        ## 连接池中的最大空闲连接
        max-idle: 20
        ## 连接池中的最小空闲连接
        min-idle: 8
    ## 连接超时时间(毫秒)
    timeout: 300000ms

redis配置类


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisConfig {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        //设置序列化
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer<Object> jacksonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();

        //指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonRedisSerializer.setObjectMapper(om);

        //配置redisTemplate
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //String类的序列化方式
        RedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);//key序列化
        template.setValueSerializer(stringSerializer);//value序列化
        template.setHashKeySerializer(stringSerializer);//Hash key序列化
        template.setHashValueSerializer(jacksonRedisSerializer);//Hash value序列化
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(factory);
        return template;
    }
}


@Data
@AllArgsConstructor
@NoArgsConstructor
public class MqModel implements Serializable {
    private String id;
    private String mes;
}

序列化工具和生产者一样, 复制过来即可

消费消息


import com.rabbitmq.client.Channel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.example.model.MqModel;
import org.example.utils.JacksonJsonUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

/**
 * explain:消费者监听
 *
 * @author Hope
 * @date 2022/6/14
 * @see ConsumerService
 */
@Slf4j
@Component
@RabbitListener(queues = "object.A")
public class ConsumerService {
    @Autowired
    RedisTemplate<String, Object> redisTemplate;

    /**
     * 消息幂等性测试方法
     *
     * @param message
     * @param channel
     * @param deliveryTag 信道中每接收一条消息自增1
     * @param redelivered true-代表已经重回过队列
     * @author Hope
     */
    @SneakyThrows
    @RabbitHandler
    public void receive(String message, Channel channel,
                        @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                        @Header(AmqpHeaders.REDELIVERED) boolean redelivered) {

        try {
            // Thread.sleep(3000);

            // 获取消息内容
            MqModel model = JacksonJsonUtil.jsonToObj(message, MqModel.class);
            // 根据消息唯一id 去redis中查询
            Boolean isKey = redisTemplate.hasKey(model.getId());


            // id不存在正常消费, 存在则丢弃
            if (!isKey) {
                log.info("message: {}, deliveryTag: {}, redelivered: {}", model, deliveryTag, redelivered);
                ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();

                //将消息的唯一ID添加到redis中
                opsForValue.set(model.getId(), model.getMes());

                // 确认消息
                channel.basicAck(deliveryTag, false);
            } else {
                log.error("消息已消费, 消息丢弃: message: {}", message);

                 /*手动ack
                 * deliveryTag 信道中每接收一条消息自增1,
                   第二个参数代表是否批量确认,我们这里只确认一条消息所以设置为false
                   第三个参数代表是否要将该消息重回队列
                 * */
                channel.basicNack(deliveryTag, false, false);
            }

        } catch (Exception e) {
            //redelivered:true-代表已经重回过队列
            if (redelivered) {
                log.info("第二次消费失败,不允许重回队列, " +
                        "message: {}, deliveryTag: {}, redelivered: {}", message, deliveryTag, redelivered);
                channel.basicNack(deliveryTag, false, false);

            } else {
                log.info("消费失败,重回队列 message: {}, deliveryTag: {}, redelivered: {}"
                        , message, deliveryTag, redelivered);
                channel.basicNack(deliveryTag, false, true);

            }

        }
    }
}

消费者最终效果

请添加图片描述

测试

消息的id已经写死, 这里只测试消息幂等性, 所以没有dao层, 启动生产者和消费者, 第一次消费redis中没有消费成功然后将id添加到redis中, 再次消费时redis中存在, 丢弃消息

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

香辣奥利奥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值