Springboot集成RabbitMQ实现消息可靠发送与可靠消费

114 阅读3分钟

一、集成RabbitMQ

1.1 引入依赖

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

1.2 配置参数

spring:
 rabbitmq:
  host: localhost
  port: 5672
  username: admin
  password: admin
  ##配置消息发送确认机制
  publisher-confirm-type: correlated
  #开启手动ack
  listener:
    simple:
      acknowledge-mode: manual

二、创建队列与交换机

2.1架构逻辑图

此例中我们创建如下图所示交换机与队列,消息投递后路由到MessageQueue与PushQueue两个队列,消费失败则投递到ORDER_DEAD_LETTER_EXCHANGE死信交换机。

image.png

1、创建MessageQueue与PushQueue两个队列,并将其绑定到Fanout Exchange交换机

2、消息发送者将消息发送给Fanout Exchange,投递到MessageQueue与PushQueue队列

3、监听者监听MessageQueue与PushQueue队列,正常消费消息

4、若消息消费失败,MessageQueue与PushQueue中消息将会投递到绑定的ORDER_DEAD_LETTER_EXCHANGE(死信交换机),路由键为编码设置的YD

5、死信交换机将消息路由到死信队列中

2.2代码实现

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * rabbitMQ队列初始化,若不存在则创建并绑定交换机.
 *
 * @author liuruojing
 * @date 2022/2/7 上午9:26
 * @since v1.0
 */
@Configuration
public class RabbitMQConfig {
    //短信推送队列,并绑定死信交换机
    @Bean
    public Queue MessageQueue() {
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", "ORDER_DEAD_LETTER_EXCHANGE");
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        return QueueBuilder.durable("order.message").withArguments(args).build();
    }

    //app推送队列,,并绑定死信交换机
    @Bean
    public Queue PushQueue() {
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", "ORDER_DEAD_LETTER_EXCHANGE");
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        return QueueBuilder.durable("order.push").withArguments(args).build();
    }

    /**
     * 声明死信队列.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/2/7 上午10:50
     * @sice v1.0
     */
    @Bean
    public Queue DeathQueue() {
        return new Queue("message.death");
    }

    /**
     * 声明一个扇形交换机.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/2/7 上午11:42
     * @sice v1.0
     */
    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("ORDER_EXCHANGE");
    }

    /**
     * 声明一个死信交换机.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/2/7 上午10:46
     * @sice v1.0
     */
    @Bean
    DirectExchange deathExchange() {
        return new DirectExchange("ORDER_DEAD_LETTER_EXCHANGE");
    }

    /**
     * 绑定MessageQueue到扇形交换机.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/2/7 下午2:38
     * @sice v1.0
     */
    @Bean
    Binding bindingExchange(Queue MessageQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(MessageQueue).to(fanoutExchange);
    }

    /**
     * 绑定PushQueue到扇形交换机.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/2/7 下午2:38
     * @sice v1.0
     */
    @Bean
    Binding bindingExchangeB(Queue PushQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(PushQueue).to(fanoutExchange);
    }

    /**
     * 绑定DeathQueue到死信交换机,并指定路由key为YD.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/2/7 上午11:51
     * @sice v1.0
     */
    @Bean
    public Binding deadLetterBindingQAD(Queue DeathQueue,
                                        DirectExchange deathExchange) {
        return BindingBuilder.bind(DeathQueue).to(deathExchange).with("YD");
    }
}

三、实现可靠消息发送

1)新建消息发送记录表

-- ----------------------------
-- Table structure for t_message_push_statu
-- ----------------------------
DROP TABLE IF EXISTS `t_message_push_statu`;
CREATE TABLE `t_message_push_statu` (
  `message_id` bigint(20) NOT NULL COMMENT '消息id',
  `message_context` text COLLATE utf8mb4_croatian_ci COMMENT '消息推送内容',
  `time` datetime DEFAULT NULL COMMENT '首次推送时间',
  `count` int(11) DEFAULT '0' COMMENT '推送次数',
  `statu` tinyint(255) DEFAULT '0' COMMENT '推送状态:0-等待回执;1-推送成功;-1-推送失败',
  `reason` text COLLATE utf8mb4_croatian_ci COMMENT '推送失败原因,当statu=-1时有效',
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_croatian_ci;

SET FOREIGN_KEY_CHECKS = 1;

2)代码实现消息推送

消息推送失败记录入库,后续业务逻辑视情况用定时任务自定义处理

/**
 * 实现可靠消息发送.
 *
 * @author liuruojing
 * @date 2022/1/14 下午7:58
 * @since v1.0
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private OrderEntityMapper orderEntityMapper;
    @Autowired
    private MessagePushStatuEntityMapper messagePushStatuEntityMapper;

    /**
     * 前置方法,初始化设置消息发送回调方法.
     *
     * @param
     * @return
     * @auther liuruojing
     * @date 2022/1/19 下午4:34
     * @sice v1.0
     */
    @PostConstruct
    private void postConstruct() {
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                log.info("CorrelationData:{},ack:{},cause:{}", correlationData, ack, cause);
                if (ack) {
                    log.info("MQ消息发布成功:" + correlationData.getId());
                    //更新状态
                    MessagePushStatuEntity mp=new MessagePushStatuEntity();
                    mp.setMessageId(Long.valueOf(correlationData.getId()));
                    mp.setStatu((byte) 1);
                    messagePushStatuEntityMapper.updateByPrimaryKey(mp);

                } else {
                    log.info("MQ消息发布失败,存储至数据库,等待定时任务重新发起或shell脚本定时统计告警。。。");
                    MessagePushStatuEntityWithBLOBs mp=messagePushStatuEntityMapper.selectByPrimaryKey(Long.valueOf(correlationData.getId()));
                    mp.setStatu((byte) -1);
                    mp.setCount(mp.getCount()+1);
                    mp.setReason(cause);
                    messagePushStatuEntityMapper.updateByPrimaryKeyWithBLOBs(mp);
                }
            }
        });
    }

    @Override
    @Transactional
    public void Order(OrderEntity order) {
        //订单入库
        int i=orderEntityMapper.insert(order);
        if(i>0){
            MessageContext ms = new MessageContext<OrderEntity>(order);
            MessagePushStatuEntityWithBLOBs messagePushStatuEntityWithBLOBs=new MessagePushStatuEntityWithBLOBs();
            messagePushStatuEntityWithBLOBs.setMessageId(ms.getMessageId());
            messagePushStatuEntityWithBLOBs.setMessageContext(JSON.toJSONString(ms));
            //消息初始化入库
            messagePushStatuEntityMapper.insert(messagePushStatuEntityWithBLOBs);
            //发送消息
            rabbitTemplate.convertAndSend("ORDER_EXCHANGE", null, JSON.toJSONString(ms), new CorrelationData(ms.getMessageId().toString()));
            log.info("[x] Sent " + JSON.toJSONString(ms));
        }else{
            throw  new RuntimeException("数据插入失败");
        }
    }
}

四、实现可靠消息消费


import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.stereotype.Component;
import java.io.IOException;

/**
 * 队列消息消费.
 *
 * @author liuruojing
 * @date 2022/1/18 下午8:20
 * @since v1.0
 */
@Component
@Slf4j
public class RabbitListener {


    @org.springframework.amqp.rabbit.annotation.RabbitListener(queues = "order.message")
    public void processMessage(String content, Message message, Channel channel) {
        long deliveryTag = -1;
        try {
            deliveryTag = message.getMessageProperties().getDeliveryTag();
            log.info("消费短信发送消息:{},消息内容:{}", deliveryTag, content);
            //todosomething
            channel.basicAck(deliveryTag, false);
            log.info("短信发送消息消费成功:{},消息内容:{}", deliveryTag, content);

        } catch (Exception e) {
            log.error("短信发送消息消费失败", e);
            try {
                //返回nack,不重发,丢弃或会进到死信队列
                channel.basicNack(deliveryTag, false, false);
            } catch (IOException exception) {
                log.error("短信发送消息拒签失败", exception);
            }
        }
    }

    @org.springframework.amqp.rabbit.annotation.RabbitListener(queues = "order.push")
    public void processPushMessage(String content, Message message, Channel channel) {
        long deliveryTag = -1;
        try {
            deliveryTag = message.getMessageProperties().getDeliveryTag();
            log.info("消费推送消息:{},消息内容:{}", deliveryTag, content);
            //todosomething
            channel.basicAck(deliveryTag, false);
            log.info("推送消息消费成功:{},消息内容:{}", deliveryTag, content);
        } catch (Exception e) {
            log.error("推送消息消费失败", e);
            try {
                //返回nack,不重发,丢弃或会进到死信队列
                channel.basicNack(deliveryTag, false, false);
            } catch (IOException exception) {
                log.error("推送消息拒签失败", exception);
            }
        }
    }
}

监听死信队列逻辑本例中未实现,可根据实际情况实现