RabbitMQ消息可靠性传输示例

一、简介

生产端的可靠性投递:

  • 保障消息的成功发出;
  • 保障MQ节点的成功接收;
  • 发送端收到MQ节点(Broker) 确认应答;
  • 完善的消息补偿机制;

在实际项目中某些场景下,必须保证消息不会出现丢失情况,这时候就需要我们对消息可靠性传输解决方法有所认识,才能在各种特殊情况下不会出现消息丢失、消息多发现象等。生产者可靠性传输有两种方案:

  • 消息落库,对消息状态进行打标
  • 消息的延迟投递,做二次确认,回调检查

本文将使用"消息落库,对消息状态进行打标"方式保证消息可靠性传输。

二、原理图

思路:

【1】业务数据以及MQ消息入库处理:首先将业务数据保存到数据库中,然后生成一条消息,也保存到消息记录表中,同时指定消息初始化状态为0(发送中)。

【2】生产者发送消息到MQ服务器,生产者端监听ConfirmCallback回调。

【3】生产者接收到Broker发送的确认应答信号,判断该条消息是否发送成功,如果成功,那么更新该条消息的状态为1(发送成功),如果发送失败的话,则进行重发尝试。

【4】假设在消息确认的时候由于网络闪断,MQ Broker端异常等原因导致 回送消息失败或者异常,导致生产者未能成功收到确认消息,针对这种情况,我们可以使用定时任务,去定时查询数据库中距离消息创建时间超过5分钟的且状态为0的消息,然后对这些消息进行重试尝试。

【5】当然有些消息可能就是由于一些实际的问题无法路由到Broker,比如routingKey设置不对,对应的队列被误删除了,那么这种消息即使重试多次也仍然无法投递成功,所以需要对重试次数做限制,比如限制3次,如果投递次数大于三次,那么就将消息状态更新为2(发送失败),表示这个消息最终投递失败,可以通过人工去处理这些失败消息。(或者把消息转储到失败消息记录表中)。

下面我们使用springboot + mybatis实现一个消息可靠性传输的案例,具体代码如下:

三、案例

【1】数据库脚本:

/*
SQLyog Ultimate v11.24 (32 bit)
MySQL - 5.5.44 : Database - rabbitmq_reliability
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`rabbitmq_reliability` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `rabbitmq_reliability`;

/*Table structure for table `broker_message_log` */

DROP TABLE IF EXISTS `broker_message_log`;

CREATE TABLE `broker_message_log` (
  `message_id` varchar(128) NOT NULL,
  `message` varchar(4000) DEFAULT NULL,
  `try_count` int(4) DEFAULT '0',
  `status` varchar(10) DEFAULT '',
  `next_retry` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `create_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Table structure for table `t_order` */

DROP TABLE IF EXISTS `t_order`;

CREATE TABLE `t_order` (
  `id` varchar(128) NOT NULL,
  `name` varchar(128) DEFAULT NULL,
  `message_id` varchar(128) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

【2】pom.xml依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.21.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wsh.springboot</groupId>
    <artifactId>springboot_rabbotmq_topic_exchange</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_rabbotmq_topic_exchange</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.4</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.49</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.2</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

【3】application.yml:主要包括rabbitmq、数据源、mybatis配置等

server:
  port: 4444
spring:
  application:
    name: rabbitmq-message-abilitity-demo
  rabbitmq:
    host: 127.0.0.1
    virtual-host: /vhost
    username: wsh
    password: wsh
    port: 5672
    publisher-confirms: true
    publisher-returns: true
    template:
      mandatory: true
    listener:
      simple:
        acknowledge-mode: manual
  datasource:
    username: root
    password: wsh0905
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/rabbitmq_reliability?characterEncoding=utf8
  http:
    encoding:
        charset: utf-8
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
    default-property-inclusion: non_null
mybatis:
  mapper-locations: classpath:mapping/*Mapper.xml
  type-aliases-package: com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity

【4】实体类OrderInfo:

import java.io.Serializable;

/**
 * @Description: 订单实体类
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 8:59
 */
public class OrderInfo implements Serializable {
    /**
     * 订单ID
     */
    private String id;
    /**
     * 订单名称
     */
    private String name;
    /**
     * 消息ID
     */
    private String messageId;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getMessageId() {
        return messageId;
    }

    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }
}

【5】实体类BrokerMessageLog:

import java.util.Date;

/**
 * @Description: 消息记录实体类
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 9:00
 */
public class BrokerMessageLog {
    /**
     * 消息ID
     */
    private String messageId;
    /**
     * 消息内容
     */
    private String message;
    /**
     * 重试次数
     */
    private Integer tryCount;
    /**
     * 消息的状态
     */
    private String status;
    /**
     * 下一次重试时间
     */
    private Date nextRetry;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;

    public String getMessageId() {
        return messageId;
    }

    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Integer getTryCount() {
        return tryCount;
    }

    public void setTryCount(Integer tryCount) {
        this.tryCount = tryCount;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Date getNextRetry() {
        return nextRetry;
    }

    public void setNextRetry(Date nextRetry) {
        this.nextRetry = nextRetry;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
}

【6】RabbitMQ配置类:声明队列、交换机、绑定关系等

/**
 * @Description: RabbitMQ配置类
 * @author: weishihuai
 * @Date: 2019/7/28 10:34
 */
@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue queue() {
        return new Queue(Constants.QUEUE_NAME);
    }

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(Constants.EXCHANGE_NAME);
    }

    @Bean
    public Binding bindQueue() {
        return BindingBuilder.bind(queue()).to(topicExchange()).with(Constants.ROUTE_KEY);
    }

}

【7】定时任务配置类

/**
 * @Description: 定时任务配置类
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 9:06
 */
@Configuration
@EnableScheduling
public class SchedulerConfiguration implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(taskScheduler());
    }

    @Bean
    public Executor taskScheduler() {
        //创建一个定长的线程池
        return Executors.newScheduledThreadPool(10);
    }

}

【8】自定义消息发送确认回调配置类:  implements RabbitTemplate.ConfirmCallback

import com.wsh.springboot.springboot_rabbotmq_topic_exchange.constant.Constants;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper.BrokerMessageLogMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;

/**
 * @Description 自定义消息发送确认的回调
 * @Author weishihuai
 * @Date 2019/7/28 10:42
 */
@Component
public class CustomConfirmAndReturnCallback implements RabbitTemplate.ConfirmCallback {
    private static final Logger logger = LoggerFactory.getLogger(CustomConfirmAndReturnCallback.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private BrokerMessageLogMapper brokerMessageLogMapper;

    /**
     * PostConstruct: 用于在依赖关系注入完成之后需要执行的方法上,以执行任何初始化.
     */
    @PostConstruct
    public void init() {
        //指定 ConfirmCallback
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 如果消息没有到达交换机,则该方法中isSendSuccess = false,error为错误信息;
     * 如果消息正确到达交换机,则该方法中isSendSuccess = true;
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean isSendSuccess, String error) {
        String messageId = correlationData.getId();
        if (isSendSuccess) {
            //如果消息到达MQ Broker,更新消息
            brokerMessageLogMapper.changeBrokerMessageLogStatus(messageId, Constants.ORDER_SEND_SUCCESS, new Date());
        } else {
            logger.error("消息发送异常...");
        }
    }

}

【9】全局常量类:

/**
 * @Description: 常量类
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 9:09
 */
public class Constants {
    /**
     * 队列名称
     */
    public static final String QUEUE_NAME = "order-queue";

    /**
     * 交换机名称
     */
    public static final String EXCHANGE_NAME = "order-exchange";

    /**
     * 错误交换机名称
     */
    public static final String ERROR_EXCHANGE_NAME = "error-order-exchange";

    /**
     * 路由键
     */
    public static final String ROUTE_KEY = "order.#";

    /**
     * 消息状态(发送中)
     */
    public static final String ORDER_SENDING = "0";

    /**
     * 消息状态(成功)
     */
    public static final String ORDER_SEND_SUCCESS = "1";

    /**
     * 消息状态(失败)
     */
    public static final String ORDER_SEND_FAILURE = "2";

    /**
     * 消息超时时间(一分钟)
     */
    public static final int ORDER_TIMEOUT = 1;

}

【10】订单Mapper持久层接口:

import com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.OrderInfo;
import org.springframework.stereotype.Repository;

/**
 * @Description: 订单Mapper
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 15:47
 */
@Repository
public interface OrderMapper {

    /**
     * 保存订单信息
     *
     * @param orderInfo 订单信息
     */
    void saveOrder(OrderInfo orderInfo);

}

【11】消息记录Mapper持久层接口:

/**
 * @Description: 消息发送记录Mapper
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 15:34
 */
@Repository
public interface BrokerMessageLogMapper {

    /**
     * 查询消息状态为0(发送中) 且已经超时的消息集合
     *
     * @return 消息集合
     */
    List<BrokerMessageLog> queryTimeoutBrokerMessageLog();

    /**
     * 更新消息重试发送次数
     *
     * @param messageId  消息ID
     * @param updateTime 更新时间
     */
    void updateBrokerMessageLogRetryCount(@Param("messageId") String messageId, @Param("updateTime") Date updateTime);

    /**
     * 更新消息发送状态
     *
     * @param messageId  消息ID
     * @param status     消息的状态
     * @param updateTime 更新时间
     */
    void changeBrokerMessageLogStatus(@Param("messageId") String messageId, @Param("status") String status, @Param("updateTime") Date updateTime);

    /**
     * 保存消息发送记录消息
     *
     * @param brokerMessageLog 消息发送记录
     */
    void saveBrokerMessageLog(BrokerMessageLog brokerMessageLog);
}

【12】订单Mapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper.OrderMapper">

    <!--保存订单信息-->
    <insert id="saveOrder"
            parameterType="com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.OrderInfo">
        insert into t_order(id,name,message_id) values(#{id},#{name},#{messageId})
    </insert>

</mapper>

【13】消息记录Mapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper.BrokerMessageLogMapper">

    <!--查询消息状态为0(发送中) 且已经超时的消息集合-->
    <select id="queryTimeoutBrokerMessageLog"
            resultType="com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.BrokerMessageLog">
              SELECT t.`create_time` AS createTime,t.`message` AS message,t.`message_id` AS messageId,t.`next_retry` AS nextRetry, t.`status` AS STATUS,t.`try_count` AS tryCount,t.`update_time` AS updateTime
                  from broker_message_log t
                  where status = '0'
                  and next_retry <![CDATA[ <= ]]> sysdate()
    </select>

    <!--更新消息重试发送次数-->
    <update id="updateBrokerMessageLogRetryCount">
        update broker_message_log bml
        set bml.try_count = bml.try_count + 1,
          bml.update_time = #{updateTime, jdbcType=TIMESTAMP}
        where bml.message_id = #{messageId,jdbcType=VARCHAR}
  </update>

    <!--更新消息发送状态-->
    <update id="changeBrokerMessageLogStatus">
        update broker_message_log bml
        set bml.status = #{status,jdbcType=VARCHAR},
              bml.update_time = #{updateTime, jdbcType=TIMESTAMP}
        where bml.message_id = #{messageId,jdbcType=VARCHAR}
  </update>

    <!--保存消息发送记录消息-->
    <insert id="saveBrokerMessageLog"
            parameterType="com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.BrokerMessageLog">
            INSERT INTO `broker_message_log`
                    (`message_id`,
                     `message`,
                     `try_count`,
                     `status`,
                     `next_retry`,
                     `create_time`,
                     `update_time`)
                VALUES (#{messageId},
                        #{message},
                        #{tryCount},
                        #{status},
                        #{nextRetry},
                        #{createTime},
                        #{updateTime})
  </insert>

</mapper>

【14】订单服务层接口OrderService:

import com.alibaba.fastjson.JSONObject;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.constant.Constants;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.BrokerMessageLog;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.OrderInfo;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper.BrokerMessageLogMapper;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper.OrderMapper;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.producer.Producer;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @Description: 订单服务层接口
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 15:36
 */
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private BrokerMessageLogMapper brokerMessageLogMapper;
    @Autowired
    private Producer producer;

    public void saveOrderInfo(OrderInfo order) throws Exception {
        Date nowDate = new Date();
        // 1.保存订单信息
        orderMapper.saveOrder(order);

        // 2.保存消息记录信息
        BrokerMessageLog brokerMessageLog = new BrokerMessageLog();
        brokerMessageLog.setMessageId(order.getMessageId());
        brokerMessageLog.setMessage(JSONObject.toJSONString(order));
        // 初始化消息状态为0
        brokerMessageLog.setStatus(Constants.ORDER_SENDING);
        // 初始化已重试次数为0
        brokerMessageLog.setTryCount(0);
        // 设置消息超时时间为一分钟
        brokerMessageLog.setNextRetry(DateUtils.addMinutes(nowDate, Constants.ORDER_TIMEOUT));
        brokerMessageLog.setCreateTime(nowDate);
        brokerMessageLog.setUpdateTime(nowDate);
        brokerMessageLogMapper.saveBrokerMessageLog(brokerMessageLog);

        // 3.消息发送者发送消息到MQ
        producer.sendOrder(order);
    }

}

【15】消息重发定时器:容器启动后,延迟5秒后执行定时器,以后每10秒再执行一次该定时器

import com.alibaba.fastjson.JSONObject;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.constant.Constants;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.BrokerMessageLog;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.OrderInfo;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper.BrokerMessageLogMapper;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.producer.Producer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;

/**
 * @Description: 消息重发定时器
 * @Author: weixiaohuai
 * @Date: 2019/7/28
 * @Time: 15:50
 */
@Component
public class MessageReSendScheduler {
    private static final Logger logger = LoggerFactory.getLogger(MessageReSendScheduler.class);

    @Autowired
    private Producer producer;

    @Autowired
    private BrokerMessageLogMapper brokerMessageLogMapper;

    /**
     * 容器启动后,延迟5秒后执行定时器,以后每10秒再执行一次该定时器
     */
    @Scheduled(initialDelay = 5000, fixedDelay = 10000)
    public void reSend() {
        //查询出状态为0以及超时的消息
        List<BrokerMessageLog> list = brokerMessageLogMapper.queryTimeoutBrokerMessageLog();
        if (null != list && list.size() > 0) {
            for (BrokerMessageLog brokerMessageLog : list) {
                if (null != brokerMessageLog) {
                    if (brokerMessageLog.getTryCount() >= 3) {
                        logger.info("消息【{}】重试三次之后仍失败..", brokerMessageLog.getMessageId());
                        //重试失败三次,更新消息状态为发送失败
                        brokerMessageLogMapper.changeBrokerMessageLogStatus(brokerMessageLog.getMessageId(), Constants.ORDER_SEND_FAILURE, new Date());
                    } else {
                        // 如果重试次数小于三次,那么进行重发
                        brokerMessageLogMapper.updateBrokerMessageLogRetryCount(brokerMessageLog.getMessageId(), new Date());
                        OrderInfo orderInfo = JSONObject.parseObject(brokerMessageLog.getMessage(), OrderInfo.class);
                        try {
                            logger.info("消息【{}】即将进行重发尝试..", brokerMessageLog.getMessageId());
                            producer.sendOrder(orderInfo);
                        } catch (Exception e) {
                            e.printStackTrace();
                            logger.error("消息处理异常..");
                        }
                    }
                }
            }
        }
    }
}

【16】消息发送者Producer:

import com.wsh.springboot.springboot_rabbotmq_topic_exchange.constant.Constants;
import com.wsh.springboot.springboot_rabbotmq_topic_exchange.entity.OrderInfo;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description: 消息发送者
 * @author: weishihuai
 * @Date: 2019/7/28 10:27
 */
@Component
public class Producer {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendOrder(OrderInfo order) throws Exception {
        //消息唯一ID
        CorrelationData correlationData = new CorrelationData(order.getMessageId());
        //正常发送消息
        rabbitTemplate.convertAndSend(Constants.EXCHANGE_NAME, "order.save", order, correlationData);

        //指定一个不存在的交换机,这样触发confirmCallback失败回调,进行重发尝试
//        rabbitTemplate.convertAndSend(Constants.ERROR_EXCHANGE_NAME, "order.save", order, correlationData);
    }
}

【17】消息消费者Consumer:

import com.wsh.springboot.springboot_rabbotmq_topic_exchange.constant.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @Description: 消息消费者
 * @author: weishihuai
 * @Date: 2019/7/28 11:30
 */
@Component
public class Consumer {
    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);

    @RabbitListener(queues = {Constants.QUEUE_NAME})
    public void receiveMessage(Message message) {
        logger.info("【消费者接收到消息:{}】", message);
    }

}

【18】应用启动类:

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.wsh.springboot.springboot_rabbotmq_topic_exchange.mapper")
public class SpringbootRabbotmqTopicExchangeApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootRabbotmqTopicExchangeApplication.class, args);
    }

}

【19】测试发送订单:验证订单业务数据以及消息记录是否正确进行入库。

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootRabbotmqTopicExchangeApplicationTests {

    @Autowired
    private Producer producer;
    @Autowired
    private OrderService orderService;

    @Test
    public void testCreateOrder() throws Exception {
        OrderInfo order = new OrderInfo();
        order.setId(UUID.randomUUID().toString());
        order.setName("图书订单");
        order.setMessageId(UUID.randomUUID().toString());
        orderService.saveOrderInfo(order);
    }

}

启动项目,并且运行上面的单元测试,通过sqlyong查询两个表数据:

订单表:

消息记录表:

可见,由于指定的routeKey正确路由,订单数据以及消息记录都成功入库,并且消息记录表中的消息状态为1(发送成功)。

【20】接着,修改一下消息发送的目标交换机,随便指定一个不存在的交换机名称,这时候消息会发生丢失,从而触发confirmCallback的失败回调,所以该条消息的状态为0(发送中)。由于此时存在定时器去定时查询状态为0并且消息已经过时的消息,进行重发尝试,如果重试三次还是未能发送成功,将更新消息状态为2,表示投递失败。

继续运行上面的测试用例,查看结果:

订单表:

消息记录表:

控制台输出:可见,此条消息重复尝试了三次之后依然失败,这时候消息记录表中的消息状态被更新为2(发送失败)。

四、总结

本文通过将消息标识状态方式进行包装消息可靠性传输,该种方式适合于数据量不会特别大的场合,在高并发下,由于需要进行两次数据入库操作,对数据库压力蛮大的,所以还有一种方式解决消息可靠性传输,就是 "消息的延迟投递,做二次确认,回调检查",有空会去研究一下。本文是在笔者看到https://www.imooc.com/article/49814大神这篇博客之后启发自己写的一个示例,也加深了自己对可靠性传输解决方案的认识。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值