手记

RabbitMQ消息可靠性投递解决方案 - 基于SpringBoot实现

2018-08-04 20:42:2830888浏览

阿神

1实战 · 5手记 · 4推荐

谈到消息的可靠性投递,无法避免的,在实际的工作中会经常碰到,比如一些核心业务需要保障消息不丢失,接下来我们看一个可靠性投递的流程图,说明可靠性投递的概念:

  • Step 1: 首先把消息信息(业务数据)存储到数据库中,紧接着,我们再把这个消息记录也存储到一张消息记录表里(或者另外一个同源数据库的消息记录表)

  • Step 2:发送消息到MQ Broker节点(采用confirm方式发送,会有异步的返回结果)

  • Step 3、4:生产者端接受MQ Broker节点返回的Confirm确认消息结果,然后进行更新消息记录表里的消息状态。比如默认Status = 0 当收到消息确认成功后,更新为1即可!

  • Step 5:但是在消息确认这个过程中可能由于网络闪断、MQ Broker端异常等原因导致 回送消息失败或者异常。这个时候就需要发送方(生产者)对消息进行可靠性投递了,保障消息不丢失,100%的投递成功!(有一种极限情况是闪断,Broker返回的成功确认消息,但是生产端由于网络闪断没收到,这个时候重新投递可能会造成消息重复,需要消费端去做幂等处理)所以我们需要有一个定时任务,(比如每5分钟拉取一下处于中间状态的消息,当然这个消息可以设置一个超时时间,比如超过1分钟 Status = 0 ,也就说明了1分钟这个时间窗口内,我们的消息没有被确认,那么会被定时任务拉取出来)

  • Step 6:接下来我们把中间状态的消息进行重新投递 retry send,继续发送消息到MQ ,当然也可能有多种原因导致发送失败

  • Step 7:我们可以采用设置最大努力尝试次数,比如投递了3次,还是失败,那么我们可以将最终状态设置为Status = 2 ,最后 交由人工解决处理此类问题(或者把消息转储到失败表中)。

接下来,我们使用SpringBoot2.x 实现这一可靠性投递策略:

废话不多说,直接上代码:

  • 数据库库表结构:订单表和消息记录表

-- 表 order 订单结构
CREATE TABLE IF NOT EXISTS `t_order` (
  `id` varchar(128) NOT NULL, -- 订单ID
  `name` varchar(128), -- 订单名称 其他业务熟悉忽略
  `message_id` varchar(128) NOT NULL, -- 消息唯一ID
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 表 broker_message_log 消息记录结构
CREATE TABLE IF NOT EXISTS `broker_message_log` (
  `message_id` varchar(128) NOT NULL, -- 消息唯一ID
  `message` varchar(4000) DEFAULT NULL, -- 消息内容
  `try_count` int(4) DEFAULT '0', -- 重试次数
  `status` varchar(10) DEFAULT '', -- 消息投递状态  0 投递中 1 投递成功   2 投递失败
  `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;
  • 整合SpringBoot 实现生产端代码如下: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>

    <groupId>com.bfxy</groupId>
    <artifactId>rabbitmq-springboot-producer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>rabbitmq-springboot-producer</name>
    <description>rabbitmq-springboot-producer</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <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-test</artifactId>
            <scope>test</scope>
        </dependency>

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

        <!-- 添加JDBC jar --> 
        <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>1.1.1</version>
        </dependency>
        <dependency>
          <groupId>tk.mybatis</groupId>
          <artifactId>mapper-spring-boot-starter</artifactId>
          <version>1.1.0</version>
        </dependency>    
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.24</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- mybatis分页插件 -->
        <dependency>  
            <groupId>com.github.miemiedev</groupId>  
            <artifactId>mybatis-paginator</artifactId>  
            <version>1.2.17</version>  
            <exclusions>
                <exclusion>
                     <groupId>org.mybatis</groupId>
                    <artifactId>mybatis</artifactId>
                </exclusion>
            </exclusions>            
        </dependency>                    
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.1.26</version>
        </dependency>    
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <scope>provided</scope>    
        </dependency>  
                <dependency>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                    <version>1.2.17</version>
                </dependency>              
    </dependencies>

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


</project>
  • application.properties配置:

  • spring.rabbitmq.addresses=192.168.11.76:5672
    spring.rabbitmq.username=guest
    spring.rabbitmq.password=guest
    spring.rabbitmq.virtual-host=/
    spring.rabbitmq.connection-timeout=15000
    
    spring.rabbitmq.publisher-confirms=true
    spring.rabbitmq.publisher-returns=true
    spring.rabbitmq.template.mandatory=true
    
    server.servlet.context-path=/
    server.port=8001
    
    spring.http.encoding.charset=UTF-8
    spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
    spring.jackson.time-zone=GMT+8
    spring.jackson.default-property-inclusion=NON_NULL
    
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=root
    
    mybatis.type-aliases-package=com.bfxy.springboot
    mybatis.mapper-locations=classpath:com/bfxy/springboot/mapping/*.xml
    
    logging.level.tk.mybatis=TRACE
  • 数据源druid.properties配置

##下面为连接池的补充设置,应用到上面所有数据源中
#初始化大小,最小,最大
druid.initialSize=5
druid.minIdle=10
druid.maxActive=300
#配置获取连接等待超时的时间
druid.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 
druid.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 1 FROM DUAL
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
#打开PSCache,并且指定每个连接上PSCache的大小
druid.poolPreparedStatements=true
druid.maxPoolPreparedStatementPerConnectionSize=20
#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 
druid.filters=stat,wall,log4j
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
druid.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#合并多个DruidDataSource的监控数据
druid.useGlobalDataSourceStat=true
  • 实体对象:

  • package com.bfxy.springboot.entity;
    
    import java.io.Serializable;
    
    public class Order implements Serializable {
    
        private static final long serialVersionUID = 9111357402963030257L;
    
        private String id;
    
        private String name;
    
        private String messageId;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id == null ? null : id.trim();
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name == null ? null : name.trim();
        }
    
        public String getMessageId() {
            return messageId;
        }
    
        public void setMessageId(String messageId) {
            this.messageId = messageId == null ? null : messageId.trim();
        }
    }
  • package com.bfxy.springboot.entity;
    
    import java.util.Date;
    
    public class BrokerMessageLog {
        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 == null ? null : messageId.trim();
        }
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message == null ? null : message.trim();
        }
    
        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 == null ? null : status.trim();
        }
    
        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;
        }
    }
  • 数据库连接池代码:

  • package com.bfxy.springboot.config.database;
    
    
    import java.sql.SQLException;
    
    import javax.sql.DataSource;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import com.alibaba.druid.pool.DruidDataSource; 
    
    @Configuration
    @EnableTransactionManagement
    public class DruidDataSourceConfig {
        
        private static Logger logger = LoggerFactory.getLogger(DruidDataSourceConfig.class);
        
        @Autowired
        private DruidDataSourceSettings druidSettings;
        
        public static String DRIVER_CLASSNAME ;
        
        @Bean
        public static PropertySourcesPlaceholderConfigurer propertyConfigure(){
            return new PropertySourcesPlaceholderConfigurer();
        }    
        
        @Bean
        public DataSource dataSource() throws SQLException {
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(druidSettings.getDriverClassName());
            DRIVER_CLASSNAME = druidSettings.getDriverClassName();
            ds.setUrl(druidSettings.getUrl());
            ds.setUsername(druidSettings.getUsername());
            ds.setPassword(druidSettings.getPassword());
            ds.setInitialSize(druidSettings.getInitialSize());
            ds.setMinIdle(druidSettings.getMinIdle());
            ds.setMaxActive(druidSettings.getMaxActive());
            ds.setTimeBetweenEvictionRunsMillis(druidSettings.getTimeBetweenEvictionRunsMillis());
            ds.setMinEvictableIdleTimeMillis(druidSettings.getMinEvictableIdleTimeMillis());
            ds.setValidationQuery(druidSettings.getValidationQuery());
            ds.setTestWhileIdle(druidSettings.isTestWhileIdle());
            ds.setTestOnBorrow(druidSettings.isTestOnBorrow());
            ds.setTestOnReturn(druidSettings.isTestOnReturn());
            ds.setPoolPreparedStatements(druidSettings.isPoolPreparedStatements());
            ds.setMaxPoolPreparedStatementPerConnectionSize(druidSettings.getMaxPoolPreparedStatementPerConnectionSize());
            ds.setFilters(druidSettings.getFilters());
            ds.setConnectionProperties(druidSettings.getConnectionProperties());
            logger.info(" druid datasource config : {} ", ds);
            return ds;
        }
    
        @Bean
        public PlatformTransactionManager transactionManager() throws Exception {
            DataSourceTransactionManager txManager = new DataSourceTransactionManager();
            txManager.setDataSource(dataSource());
            return txManager;
        }
        
    }
package com.bfxy.springboot.config.database;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix="spring.datasource") 
@PropertySource("classpath:druid.properties")
public class DruidDataSourceSettings {

    private String driverClassName;
    private String url;
    private String username;
    private String password;
    
    @Value("${druid.initialSize}")
    private int initialSize;
    
    @Value("${druid.minIdle}")
    private int minIdle;
    
    @Value("${druid.maxActive}")
    private int maxActive;
    
    @Value("${druid.timeBetweenEvictionRunsMillis}")
    private long timeBetweenEvictionRunsMillis;
    
    @Value("${druid.minEvictableIdleTimeMillis}")
    private long minEvictableIdleTimeMillis;
    
    @Value("${druid.validationQuery}")
    private String validationQuery;
    
    @Value("${druid.testWhileIdle}")
    private boolean testWhileIdle;
    
    @Value("${druid.testOnBorrow}")
    private boolean testOnBorrow;
    
    @Value("${druid.testOnReturn}")
    private boolean testOnReturn;
    
    @Value("${druid.poolPreparedStatements}")
    private boolean poolPreparedStatements;
    
    @Value("${druid.maxPoolPreparedStatementPerConnectionSize}")
    private int maxPoolPreparedStatementPerConnectionSize;
    
    @Value("${druid.filters}")
    private String filters;
    
    @Value("${druid.connectionProperties}")
    private String connectionProperties;
    
    @Bean
    public static PropertySourcesPlaceholderConfigurer properdtyConfigure(){
        return new PropertySourcesPlaceholderConfigurer();
    }
    
    public String getDriverClassName() {
        return driverClassName;
    }
    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public int getInitialSize() {
        return initialSize;
    }
    public void setInitialSize(int initialSize) {
        this.initialSize = initialSize;
    }
    public int getMinIdle() {
        return minIdle;
    }
    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }
    public int getMaxActive() {
        return maxActive;
    }
    public void setMaxActive(int maxActive) {
        this.maxActive = maxActive;
    }
    public long getTimeBetweenEvictionRunsMillis() {
        return timeBetweenEvictionRunsMillis;
    }
    public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }
    public long getMinEvictableIdleTimeMillis() {
        return minEvictableIdleTimeMillis;
    }
    public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }
    public String getValidationQuery() {
        return validationQuery;
    }
    public void setValidationQuery(String validationQuery) {
        this.validationQuery = validationQuery;
    }
    public boolean isTestWhileIdle() {
        return testWhileIdle;
    }
    public void setTestWhileIdle(boolean testWhileIdle) {
        this.testWhileIdle = testWhileIdle;
    }
    public boolean isTestOnBorrow() {
        return testOnBorrow;
    }
    public void setTestOnBorrow(boolean testOnBorrow) {
        this.testOnBorrow = testOnBorrow;
    }
    public boolean isTestOnReturn() {
        return testOnReturn;
    }
    public void setTestOnReturn(boolean testOnReturn) {
        this.testOnReturn = testOnReturn;
    }
    public boolean isPoolPreparedStatements() {
        return poolPreparedStatements;
    }
    public void setPoolPreparedStatements(boolean poolPreparedStatements) {
        this.poolPreparedStatements = poolPreparedStatements;
    }
    public int getMaxPoolPreparedStatementPerConnectionSize() {
        return maxPoolPreparedStatementPerConnectionSize;
    }
    public void setMaxPoolPreparedStatementPerConnectionSize(
            int maxPoolPreparedStatementPerConnectionSize) {
        this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
    }
    public String getFilters() {
        return filters;
    }
    public void setFilters(String filters) {
        this.filters = filters;
    }
    public String getConnectionProperties() {
        return connectionProperties;
    }
    public void setConnectionProperties(String connectionProperties) {
        this.connectionProperties = connectionProperties;
    }
    
}
package com.bfxy.springboot.config.database;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

@Configuration
public class MybatisDataSourceConfig {
    
    @Autowired
    private DataSource dataSource;
    
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean() {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // 添加XML目录
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try {
            bean.setMapperLocations(resolver.getResources("classpath:com/bfxy/springboot/mapping/*.xml"));
            SqlSessionFactory sqlSessionFactory = bean.getObject();
            sqlSessionFactory.getConfiguration().setCacheEnabled(Boolean.TRUE);
            
            return sqlSessionFactory;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}
package com.bfxy.springboot.config.database;

import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@AutoConfigureAfter(MybatisDataSourceConfig.class)
public class MybatisMapperScanerConfig {
    
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
        mapperScannerConfigurer.setBasePackage("com.bfxy.springboot.mapper");
        return mapperScannerConfigurer;
    }

}
  • 定时任务配置代码:

  • package com.bfxy.springboot.config.task;
    
    import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    
    @Configuration
    @EnableScheduling
    public class TaskSchedulerConfig implements SchedulingConfigurer {
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(taskScheduler());
        }
        
        @Bean(destroyMethod="shutdown")
        public Executor taskScheduler(){
            return Executors.newScheduledThreadPool(100);
        }
    
    }
  • 常量类:

package com.bfxy.springboot.constant;

public final class Constants {

    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; /*分钟超时单位:min*/
}
  • 消息记录表核心业务:

package com.bfxy.springboot.mapper;

import java.util.Date;

import org.apache.ibatis.annotations.Param;

import com.bfxy.springboot.entity.BrokerMessageLog;
import com.sun.tools.javac.util.List;

public interface BrokerMessageLogMapper {
    /**
     * 查询消息状态为0(发送中) 且已经超时的消息集合
     * @return
     */
    List<BrokerMessageLog> query4StatusAndTimeoutMessage();
    
    /**
     * 重新发送统计count发送次数 +1
     * @param messageId
     * @param updateTime
     */
    void update4ReSend(@Param("messageId")String messageId, @Param("updateTime")Date updateTime);
    /**
     * 更新最终消息发送结果 成功 or 失败
     * @param messageId
     * @param status
     * @param updateTime
     */
    void changeBrokerMessageLogStatus(@Param("messageId")String messageId, @Param("status")String status, @Param("updateTime")Date updateTime);


    
}
  • 对应的SQL代码:

    <select id="query4StatusAndTimeoutMessage" resultMap="BaseResultMap">
          <![CDATA[  
          select message_id, message, try_count, status, next_retry, create_time, update_time
              from broker_message_log bml 
              where status = '0'
              and next_retry <= sysdate() 
          ]]> 
    </select>
  
  <update id="update4ReSend" >
    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>
  • 核心发送代码:orderService

  • package com.bfxy.springboot.service;
    
    import java.util.Date;
    
    import org.apache.commons.lang3.time.DateUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.bfxy.springboot.constant.Constants;
    import com.bfxy.springboot.entity.BrokerMessageLog;
    import com.bfxy.springboot.entity.Order;
    import com.bfxy.springboot.mapper.BrokerMessageLogMapper;
    import com.bfxy.springboot.mapper.OrderMapper;
    import com.bfxy.springboot.producer.RabbitOrderSender;
    import com.bfxy.springboot.utils.FastJsonConvertUtil;
    
    @Service
    public class OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
        
        @Autowired
        private BrokerMessageLogMapper brokerMessageLogMapper;
        
        @Autowired
        private RabbitOrderSender rabbitOrderSender;
        
        public void createOrder(Order order) throws Exception {
            // 使用当前时间当做订单创建时间(为了模拟一下简化)
            Date orderTime = new Date();
            // 插入业务数据
            orderMapper.insert(order);
            // 插入消息记录表数据
            BrokerMessageLog brokerMessageLog = new BrokerMessageLog();
            // 消息唯一ID
            brokerMessageLog.setMessageId(order.getMessageId());
            // 保存消息整体 转为JSON 格式存储入库
            brokerMessageLog.setMessage(FastJsonConvertUtil.convertObjectToJSON(order));
             // 设置消息状态为0 表示发送中
            brokerMessageLog.setStatus("0");
             // 设置消息未确认超时时间窗口为 一分钟 
            brokerMessageLog.setNextRetry(DateUtils.addMinutes(orderTime, Constants.ORDER_TIMEOUT));
            brokerMessageLog.setCreateTime(new Date());
            brokerMessageLog.setUpdateTime(new Date());
            brokerMessageLogMapper.insert(brokerMessageLog);
            // 发送消息
            rabbitOrderSender.sendOrder(order);
        }
        
    }
  • MQ消息发送核心代码:

package com.bfxy.springboot.producer;

import java.util.Date;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ConfirmCallback;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bfxy.springboot.constant.Constants;
import com.bfxy.springboot.entity.Order;
import com.bfxy.springboot.mapper.BrokerMessageLogMapper;
import com.bfxy.springboot.mapper.OrderMapper;

@Component
public class RabbitOrderSender {

    //自动注入RabbitTemplate模板类
    @Autowired
    private RabbitTemplate rabbitTemplate;  
    
    @Autowired
    private BrokerMessageLogMapper brokerMessageLogMapper;
    
    //回调函数: confirm确认
    final ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            System.err.println("correlationData: " + correlationData);
            String messageId = correlationData.getId();
            if(ack){
                //如果confirm返回成功 则进行更新
                brokerMessageLogMapper.changeBrokerMessageLogStatus(messageId, Constants.ORDER_SEND_SUCCESS, new Date());
            } else {
                //失败则进行具体的后续操作:重试 或者补偿等手段
                System.err.println("异常处理...");
            }
        }
    };
    
    //发送消息方法调用: 构建自定义对象消息
    public void sendOrder(Order order) throws Exception {
        rabbitTemplate.setConfirmCallback(confirmCallback);
        //消息唯一ID
        CorrelationData correlationData = new CorrelationData(order.getMessage_id());
        rabbitTemplate.convertAndSend("order-exchange", "order.ABC", order, correlationData);
    }
    
}
  • 消息重试、最大努力尝试策略(定时任务):

  • package com.bfxy.springboot.task;
    
    import java.util.Date;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import com.bfxy.springboot.constant.Constants;
    import com.bfxy.springboot.entity.BrokerMessageLog;
    import com.bfxy.springboot.entity.Order;
    import com.bfxy.springboot.mapper.BrokerMessageLogMapper;
    import com.bfxy.springboot.producer.RabbitOrderSender;
    import com.bfxy.springboot.utils.FastJsonConvertUtil;
    
    @Component
    public class RetryMessageTasker {
    
        
        @Autowired
        private RabbitOrderSender rabbitOrderSender;
        
        @Autowired
        private BrokerMessageLogMapper brokerMessageLogMapper;
        
        @Scheduled(initialDelay = 5000, fixedDelay = 10000)
        public void reSend(){
            //pull status = 0 and timeout message 
            List<BrokerMessageLog> list = brokerMessageLogMapper.query4StatusAndTimeoutMessage();
            list.forEach(messageLog -> {
                if(messageLog.getTryCount() >= 3){
                    //update fail message 
                    brokerMessageLogMapper.changeBrokerMessageLogStatus(messageLog.getMessageId(), Constants.ORDER_SEND_FAILURE, new Date());
                } else {
                    // resend 
                    brokerMessageLogMapper.update4ReSend(messageLog.getMessageId(),  new Date());
                    Order reSendOrder = FastJsonConvertUtil.convertJSONToObject(messageLog.getMessage(), Order.class);
                    try {
                        rabbitOrderSender.sendOrder(reSendOrder);
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.err.println("-----------异常处理-----------");
                    }
                }            
            });
        }
    }
  • 测试发送订单:

  • 代码如下:

    @Autowired
    private RabbitOrderSender rabbitOrderSender;
    
    @Test
    public void testSender2() throws Exception {
         Order order = new Order();
         order.setId("2018080400000001");
         order.setName("测试订单");
         order.setMessage_id(System.currentTimeMillis() + "$" + UUID.randomUUID().toString());
         rabbitOrderSender.sendOrder(order);
    }
  • 监控台查看消息:

  • 发送成功! 现在测试 发送订单并且入库(业务库和消息记录库)

    @Autowired
    private OrderService orderService;
    
    @Test
    public void testCreateOrder() throws Exception {
         Order order = new Order();
         order.setId("2018080400000002");
         order.setName("测试创建订单");
         order.setMessageId(System.currentTimeMillis() + "$" + UUID.randomUUID().toString());
        orderService.createOrder(order);
    }
  • 发送成功 并且入库OK:业务表 和 消息记录表均有数据 且status状态=1 为成功!

  • 业务表:

  • 消息记录表:

  • 测试失败情况:修改路由KEY为 无法路由即可!

  • 这样消息就算失败的情况了。然后ACK的时候就会走异常处理,消息记录表如下:


  • 最后我们测试重试策略:直接启动生产者应用,开启定时任务,重试几次后,库表信息变化如下:


  • 最终重试3次 失败结果更新 status = 2


RabbitMQ消息中间件技术精讲:RabbitMQ是目前主流的消息中间件,非常适用于高并发环境。各大互联网公司都在使用的MQ技术,晋级技术骨干、团队核心的必备技术!


100人推荐
随时随地看视频
慕课网APP

热门评论

连sql都是有问题的……?

老师,你好,生产端的ConfirmCallback回调函数里面ack一直返回false,但是在管控台看到队列里确实收到了消息,并且消费端也能成功消费消息,但就是生产端的ConfirmCallback回调里面的ack一直返回false,具体原因

cause=clean channel shutdown; protocol method: #method<channel.close>(reply-code=200, reply-text=OK, class-id=0, method-id=0)

连sql都是有问题的……?

查看全部评论