SpringBoot整合 RabbitMQ

一、开发生产端

1、配置队列、交换机并绑定
spring:
  rabbitmq:
    virtual-host: /myhost
    port: 5672
    host: localhost
    username: admin
    password: admin
2、导入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
3、配置队列、交换机并绑定
@Configuration
public class MqConfig {
    public static final String MSG_QUEUE = "msg-queue";
    public static final String MSG_EXCHANGE = "msg-exchange";
    public static final String MSG_ROUTE_KEY = "msg.key";
    /**
     * 声明队列
     */
    @Bean
    public Queue msgQueue() {
        return new Queue(MSG_QUEUE,true,false,false);
    }
    /**
     * 声明交换机
     */
    @Bean
    public TopicExchange msgExchage() {
        //参数2是否持久化,参数3是否自动删除
        return new TopicExchange(MSG_EXCHANGE,true,false);
    }
    /**
     * 将队列和交换机进行绑定
     */
    @Bean
    public Binding bindMsgQueue() {
        return BindingBuilder.bind(msgQueue()).to(msgExchage()).with(MSG_ROUTE_KEY);
    }
}
4、发送消息
@Component
public class MsgProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void sendMsg(String message) {
        rabbitTemplate.convertAndSend(MqConfig.MSG_EXCHANGE,MqConfig.MSG_ROUTE_KEY,message);
    }
}

二、开发消费端

        同样导入依赖、配置yml

处理消息
@Component
public class MsgConsumer {
    public static final String MSG_QUEUE = "msg-queue";
    public static final String MSG_EXCHANGE = "msg-exchange";
    //消息处理的方法
    @RabbitHandler
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name=MSG_QUEUE,declare = "true",durable = "true",exclusive = "false",autoDelete = "false"),
            exchange = @Exchange(name = MSG_EXCHANGE,type = ExchangeTypes.TOPIC),
            key = "msg.*"
    ))
    //@Payload表示消费者处理的消息
    //@Headers注解表示接收的消息头将会被绑定到`headers`参数上
    public void receiveMsg(@Payload String msg, Channel channel, @Headers Map headers) {
        System.out.println("消费者处理消息:" + msg);
        Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            channel.basicAck(tag,false);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

@RabbitHandler注解:表示该方法是一个消息处理方法

        bindings属性用于指定队列和交换机的绑定关系。  

@RabbitListener注解:表示该类是一个消息监听器,用于监听指定的队列

        value属性用于指定队列的属性,包括队列的名称、是否需要声明、是否持久化、是否排他、是否自动删除等

        exchange属性用于指定交换机的名称和类型 

      key属性用于指定消息的路由键

三、 生产者端消息确认和回退 

1、yml配置
spring:
  rabbitmq:
    virtual-host: /myhost
    port: 5672
    host: localhost
    username: admin
    password: admin
    publisher-confirm-type: correlated
    publisher-returns: true
ConfirmType
NONE禁用发布确认模式,是默认值。
CORRELATED将消息成功发布到交换器后触发回调方法。
SIMPLE与CORRELATED相似,也会在将消息成功发布到交换器后触发回调方法。
2、定义消息确认回调的方法 
(1)配置类添加配置
    RabbitTemplate.ConfirmCallback callback = new RabbitTemplate.ConfirmCallback() {
        @Override
        public void confirm(CorrelationData correlationData, boolean isAck, String cause) {
            if (!isAck) {
                System.out.println("拒收的原因:" + cause);
            } else {
                if (correlationData != null) {
                    System.out.println("broker接收消息自定义ID:" + correlationData.getId());
                }
            }
        }
    };
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
        RabbitTemplate template = new RabbitTemplate();
        template.setConnectionFactory(factory);
        template.setConfirmCallback(callback);
        return template;
    }
(2)修改发送消息的方法,携带附加数据
@Component
public class MsgProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void sendMsg(String message) {
        //自定义的附加数据
        CorrelationData data = new CorrelationData();
        data.setId("10001");
        rabbitTemplate.convertAndSend(MqConfig.MSG_EXCHANGE,MqConfig.MSG_ROUTE_KEY,message,data);
    }
}
3、消息回退

        在配置文件中添加 publisher-returns: true 配置消息回退

        定义处理消息回退的方法

    RabbitTemplate.ReturnsCallback returnsCallback = new RabbitTemplate.ReturnsCallback() {
        @Override
        public void returnedMessage(ReturnedMessage msg) {
            System.out.println("--------消息路由失败------------");
            System.out.println("消息主体:" + msg.getMessage());
            System.out.println("返回编码:" + msg.getReplyCode());
            System.out.println("描述信息:" + msg.getReplyText());
            System.out.println("交换机:" + msg.getExchange());
            System.out.println("路由key:" + msg.getExchange());
            System.out.println("------------------------------");
        }
    };
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
        RabbitTemplate template = new RabbitTemplate();
        template.setConnectionFactory(factory);
        template.setConfirmCallback(callback);
        template.setReturnsCallback(returnsCallback);
        // true 表示消息通过交换机无法路由到队列时候,会把消息返回给生产者
        // false 消息无法路由到队列就直接丢弃
        template.setMandatory(true);
        return template;
    }

四.消息消费可靠性保障

常见情况问题环节解决方案
生产者消息没到交换机生产者丢失消息为上面的异步监听confirm-type、publisher-returns
交换机没有把消息路由到队列生产者丢失消息
RabbitMQ 宕机导致队列、队列中的消息丢失RabbitMQ 丢失消息

设置持久化将消息写出磁盘,否则RabbitMQ重启后所有队列和消息都会丢失

消费者消费出现异常,业务没执行消费者丢失消息

 消费者丢失消息具体处理办法:

消费者丢数据一般是因为采用了自动确认消息模式。MQ收到确认消息后会删除消息,如果这时消费者异常了,那消息就没了。使用ack机制,默认情况下自动应答,可以使用手动ack

1、修改配置
    listener:
      simple:
        acknowledge-mode: manual #开启消费者手动确认模式 (channel.bacisAck)
 2 然后在消费端代码中手动应答签收消息

如果消息消费失败,不执行消息确认代码,用channel的basicNack方法拒收

    public void receiveMsg(@Payload String msg, Channel channel, @Headers Map headers) {
        System.out.println("消费者处理消息:" + msg);
        Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            System.out.println(2/0);
            //其中`tag`是消息的唯一标识,`false`表示只确认当前消息,不确认之前的所有未确认消息。
            channel.basicAck(tag,false);
        } catch (Exception e) {
            System.out.println("签收失败");
            try {
                //其中`tag`是消息的唯一标识,`false`表示只拒绝当前消息,`true`表示该消息将重新进入队列等待被消费。
                channel.basicNack(tag,false,true);
            } catch (IOException ex) {
                System.out.println("拒收失败");
            }
        }
    }

通常的代码报错并不能因为重试而解决,可能会造成死循环

解决办法:       

  1. 当消费失败后将此消息存到 Redis,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存;
  2. basicNack方法的参数3直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理
  3. 不启用手动 ack ,使用 SpringBoot 提供的消息重试
3 使用 SpringBoot 提供的消息重试
    listener:
      simple:
        retry:
          enabled: true
          max-attempts: 3 #重试次数

注意:要抛异常,因为SpringBoot 触发重试是根据方法中发生未捕捉的异常来决定的

    public void receiveMsg(@Payload String msg, Channel channel, @Headers Map headers) {
        System.out.println("消费者处理消息:" + msg);
        Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            System.out.println(2/0);
            //channel.basicAck(tag,false);
        } catch (Exception e) {
            System.out.println("签收失败");
             //记录日志、发送邮件、保存消息到数据库,落库之前判断如果消息已经落库就不保存
            throw new RuntimeException(e);
        }
    }
4、消息重复消费(消息幂等性)

        使用手动恢复MQ解决了消息在消费者端丢失的问题,但是如果消费者处理消息成功后,由于网络波动导致手动回复MQ失败,该条消息还保存在消息队列中,由于MQ消息的重发机制,该消息会被重复消费,造成不好的后果

(1)确保消费端只执行一次:使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识

(2)允许消费端执行多次,保证数据不受影响

  • 数据库唯一约束:如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号,如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入两条;
  • 数据库乐观锁

五、消息转换器

1、需求描述:

 前面我们发送的消息都是字符串,如果想发送对象,就要用到消息转换器。

消息转换器(Message Converter)是用于将消息在生产者和消费者之间进行序列化和反序列化的组件。在消息传递过程中,生产者将消息对象转换为字节流发送到消息队列,而消费者则将接收到的字节流转换回消息对象进行处理。

 创建生产者发送消息方法

/**创建生产者发送消息方法 */
@Component
public class OrderProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void sendOrder(OrderDTO orderDTO) {
        rabbitTemplate.convertAndSend(MqConfig.ORDER_EXCHANGE,MqConfig.ORDER_KEY,orderDTO);
    }
}


//test
//发送消息

@Test
void testSendOrder() {
    OrderDTO orderDTO = new OrderDTO();
    orderDTO.setOrderSn(UUID.randomUUID().toString());
    orderDTO.setUsername("user");
    orderDTO.setAmount(new BigDecimal("200"));
    orderDTO.setCreateDate(new Date());
    orderProducer.sendOrder(orderDTO);
}

 控制台查看消息:

        上面采用的是JDK序列化方式,可以看出虽然获得了对象,但是得到的数据体积大,可读性差,为了解决这个问题,我们可以通过SpringAMQP的MessageConverter来处理

 2、Spring的消息转换器

        Spring AMQP提供了多种消息转换器(Message Converter)这些消息转换器使得消息的发送和接收可以使用不同的消息格式,如JSON、XML等,从而更灵活地处理消息数据

(1)生产端配置消息转换器
@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
    RabbitTemplate template = new RabbitTemplate();
    template.setConnectionFactory(factory);
    template.setConfirmCallback(callback);
    template.setReturnsCallback(returnsCallback);
    // true 表示消息通过交换机无法路由到队列时候,会把消息返回给生产者
    // false 消息无法路由到队列就直接丢弃
    template.setMandatory(true);
    template.setMessageConverter(messageConverter());
    return template;
}
(2)消费端配置消息转换器

添加依赖

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

配置消息转换器

@Configuration
public class MqConfig implements RabbitListenerConfigurer {
    @Resource
    private ObjectMapper objectMapper;
    //将消息转换为JSON格式
    public MappingJackson2MessageConverter messageConverter(){
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setObjectMapper(objectMapper);
        return converter;
    }
 
    @Bean
    public MessageHandlerMethodFactory messageHandlerMethodFactory(){
        DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
        factory.setMessageConverter(messageConverter());
        return factory;
    }
    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar rabbitListenerEndpointRegistrar) {
        rabbitListenerEndpointRegistrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
    }
}

创建消费者处理消息方法

    @RabbitHandler
    @RabbitListener(queues = "msg-queue")
    public void receiveOrder(@Payload OrderDTO orderDTO, Channel channel, @Headers Map map){
        System.out.println("Order消息处理:"+orderDTO);
        Long tag = (Long)map.get(AmqpHeaders.DELIVERY_TAG);
        try {
            channel.basicAck(tag,false);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

六、延迟队列

        延时队列就是用来存放需要在指定时间内被处理的消息的队列,是死信队列的一种

应用场景:

1、订单在十分钟之内未支付则自动取消

2、预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

3、用户发起退款,如果三天内没有得到处理则通知

4、用户注册成功后,如果三天内没有登陆则进行短信提醒

1 死信队列

        死信队列(Dead Letter Queue,简称DLQ)是一种用于处理消息处理失败或被拒绝的消息的特殊队列。当消息在队列中满足一定条件时,例如消息被消费者拒绝、消息过期、消息处理超时等,这些消息将被发送到死信队列而不是直接被丢弃或忽略

 

2 TTL消息

        TTL(Time To Live)指定消息在队列中存活的时间,超过指定的时间后如果消息还未被消费者消费,则该消息会被自动丢弃或转移到死信队列

(1)生产端配置类配置TTL消息
@Bean
public Queue ttlQueue() {
    Map map = new HashMap();
    map.put("x-message-ttl",5000);
    return new Queue("ttl-queue",false,false,false,map);
}
@Bean
public TopicExchange ttlExchange() {
    return new TopicExchange("ttl-exchange");
}
@Bean
public Binding bindTllQueue() {
    return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl.*");
}
(2)创建生产者发送消息方法
public void sendMessage() {
    rabbitTemplate.convertAndSend("ttl-exchange","ttl.msg","hello world");
}

3 死信交换机

(1)生产端配置类配置死信交换机

        配置死信队列、交换机并把二者绑定,修改TTL队列失效时放到死信交换机中进而存到死信队列中,而不是直接销毁

@Bean
public Queue ttlQueue(){
    Map map = new HashMap<>();
    map.put("x-message-ttl",5000);
    map.put("x-dead-letter-exchange","dead-exchange"); //死信交换机
    map.put("x-dead-letter-routing-key","dead.msg"); //发送消息时携带路由key
    return new Queue("ttl-queue",false,false,false,map);
}
@Bean
public TopicExchange deadExchange() {
    return new TopicExchange("dead-exchange");
}
@Bean
public Queue deadQueue() {
    return new Queue("dead-queue");
}
@Bean
public Binding bindDeadQueue() {
    return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead.#");
}
(2)接收端处理消息
@RabbitHandler
@RabbitListener(queues = {"dead-queue"})
public void receiveDeadMsg(@Payload String msg, Channel channel, @Headers Map headers) {
    Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
    System.out.println("处理了已经超时的消息:" + msg);
    try{
        channel.basicAck(tag,false);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

七、延迟插件

  RabbitMQ实现延迟消息的方式有两种,一种是使用死信队列,另一种是使用延迟插件。 

        通过安装插件,自定义交换机,让交换机拥有延迟发送消息的能力,从而实现延迟消息,相较于死信队列延迟插件只需创建一个交换机和一个队列,使用起来简单

1、延迟插件下载安装

https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases

将插件文件复制到RabbitMQ安装目录的plugins目录下,然后进入RabbitMQ安装目录的sbin目录下,使用如下命令启用延迟插件;

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

2 SpringBoot中实现延迟插件

(1) 开发生产者端

配置交换机、队列和绑定关系

    /**
     * 订单延迟插件消息队列所绑定的交换机
     */
    @Bean
    DirectExchange orderCancelExchange() {
       return  ExchangeBuilder.directExchange("order-delay-exchange")
               .delayed().durable(true)
                .build();
    }
    /**
     * 订单延迟插件队列
     */
    @Bean
    public Queue orderCancelQueue() {
        return new Queue("order-delay-queue");
    }
    /**
     * 将订单延迟插件队列绑定到交换机
     */
    @Bean
    public Binding bindOrderCancelQueue() {
        return BindingBuilder.bind(orderCancelQueue())
                .to(orderCancelExchange()).with("delay.order.key");
    }

创建发送消息方法

通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间 

@Component
public class CancelOrderSender {
    @Resource
    private RabbitTemplate rabbitTemplate;
    public void sendMessage(Long orderId,Long delayTime) {
        rabbitTemplate.convertAndSend("order-delay-exchange", "delay.order.key", orderId, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //给消息设置延迟毫秒值
                message.getMessageProperties().setHeader("x-delay",delayTime);
                return message;
            }
        });
    }
}

  Service层

@Service
public class OrderServiceImpl {
    @Resource
    private CancelOrderSender cancelOrderSender;
    public void createOrder() {
        System.out.println("下单后生成订单ID");
        Long orderId = 1001L;
        sendDelayMessageCancelOrder(orderId);
    }
    public void sendDelayMessageCancelOrder(Long orderId) {
        //获取订单超时时间,假设为5秒
        long delayTimes = 5 * 1000;
        cancelOrderSender.sendMessage(orderId,delayTimes);
    }
}

Controller层

@RestController
@RequestMapping("/order")
public class OrderController {
    @Resource
    private OrderServiceImpl orderService;
    @PostMapping
    public String create() {
        orderService.createOrder();
        return "success";
    }
}
(2)、开发消费者端

 Service层

@Service
public class OrderServiceImpl {
    public void cancelOrder(Long orderId) {
        System.out.println("查询订单编号为:" + orderId + "订单状态,如果是待支付状态,则更新为已失效");
    }
}

创建处理消息的方法

@Component
public class CancelOrderReceiver {
    @Autowired
    private OrderServiceImpl orderService;
    @RabbitHandler
    @RabbitListener(queues =  {"order-delay-queue"})
    public void handle(@Payload Long orderId, Channel channel, @Headers Map headers) {
        orderService.cancelOrder(orderId);
        Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            channel.basicAck(tag,false);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值