rabbitMq 在 springboot 当中的生产消息确认机制

404 阅读3分钟

       最近在做的一个项目,为了应对压力测试,在项目中引入消息中间件,用来削峰填谷。本篇就简单的介绍一下我在这个过程中的一些实现和体会。

1、消息中间件

       消息中间件有很多,目前比较流行的有 rabbitMq,rocketMq,activeMq,kafka等。鉴于rabbitMq 成熟、稳定、支持多种工作模式、消息持久化等特点,和Springboot 对 rabbitMq 的支持也比较友好。所以我这边选用的是rabbitMq。

       众所周知,消息中间件一般有 业务解耦,流量削峰填谷等作用。在内外网的环境中,有时也可作为数据传递的较为安全的中转方式。

2、实现

       闲言少叙,直接引入实现的过程。        因为引入 rabbitMq 的主要目的是流量控制,因此在这里的主要想法还是使用生产消费者模式,创建多个 Queue 用于业务数据存储。虽然,rabbitMq 拥有 direct、fallout、topic、headers 等工作模式,在此处,direct 方式就可以满足需要。

       在Springboot 当中,直接在配置文件中,配置rabbitMq 信息,就可以初始化 rabbitTemplate。 最开始,我也是这么操作的。

# rabbitmq 配置
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest
  #虚拟host  可以不设置 默认host
  virtual-host: /
  # 确认消息是否发送到交换机
  publisher-confirm-type: correlated
  # 确认消息是否发送到队列
  publisher-returns: true
  template:
    # 消息发送失败返回到队列中,yml 文件中需要配置  publisher-returns: true
    mandatory: false

然后就是创建 交换器(exchange )、队列(queue);然后 使用 rountingkey 绑定(binding)。

public enum ExchangeEnum {

    EXCHANGE_LOGIN(MsgTypeEnum.login,ExchangeNameCons.EXCHANGE_NAME_LOGIN,ExchangeTypeEnum.DIRECT,true,false,false)
    ,EXCHANGE_STARTWORKFLOW(MsgTypeEnum.startworkflow,ExchangeNameCons.EXCHANGE_NAME_STARTWORKFLOW,ExchangeTypeEnum.DIRECT,true,false,false);

    /** 交换机业务类型 */
    private MsgTypeEnum msgTypeEnum;
    /** 交换机名称 */
    private String exchangeName;
    /** 交换机类型 */
    private ExchangeTypeEnum exchangeTypeEnum;
    /** 是否持久化交换机  */
    private boolean durable;
    /** 是否自动删除  */
    private boolean autoDel;
    /** 是否延迟  */
    private boolean delayed;
    /** headers 参数 */
    private Map<String, Object> arguments;


    ExchangeEnum(MsgTypeEnum msgTypeEnum,String exchangeName, ExchangeTypeEnum exchangeTypeEnum, boolean durable,boolean autoDel,boolean delayed) {

        this.msgTypeEnum = msgTypeEnum;
        this.exchangeName = exchangeName;
        this.exchangeTypeEnum = exchangeTypeEnum;
        this.durable = durable;
        this.autoDel = autoDel;
        this.delayed = delayed;
    }

    public Map<String, Object> getArguments() {
        return arguments;
    }

    public boolean isDelayed() {
        return delayed;
    }

    public String getExchangeName() {
        return exchangeName;
    }

    public ExchangeTypeEnum getExchangeTypeEnum() {
        return exchangeTypeEnum;
    }

    public boolean isDurable() {
        return durable;
    }


    public MsgTypeEnum getMsgTypeEnum() {
        return msgTypeEnum;
    }

    public boolean isAutoDel() {
        return autoDel;
    }

    public static List<ExchangeEnum> toList(){
        List<ExchangeEnum> list = new ArrayList<>();
        for (ExchangeEnum exchangeEnum: ExchangeEnum.values()
        ) {

            list.add(exchangeEnum);
        }
        return list;
    }
}
@Configuration
public class RabbitMqConfig {

    /** 登陆 */
    @Bean(name = "loginQueue")
    public Queue loginQueue() {

        return new Queue(QueueEnum.QUEUE_LOGIN.getName(),QueueEnum.QUEUE_LOGIN.isDurable(),
                QueueEnum.QUEUE_LOGIN.isExclusive(), QueueEnum.QUEUE_LOGIN.isAutoDel());
    }
    @Bean(name = "loginExchange")
    public Exchange loginExchange(){

        return transExchage(ExchangeEnum.EXCHANGE_LOGIN);
    }
    @Bean(name = "loginBinding")
    public Binding loginBinding(@Qualifier("loginQueue") Queue queue,@Qualifier("loginExchange") Exchange exchange) {

        return BindingBuilder.bind(queue).to(exchange).with(RountingKeyCons.LOGIN).noargs();
    }

    /** 启动工作流 */
    @Bean(name = "startworkflowQueue")
    public Queue startworkflowQueue() {

        return new Queue(QueueEnum.QUEUE_STARTWORKFLOW.getName(),QueueEnum.QUEUE_STARTWORKFLOW.isDurable(),
                QueueEnum.QUEUE_STARTWORKFLOW.isExclusive(), QueueEnum.QUEUE_STARTWORKFLOW.isAutoDel());
    }
    @Bean(name = "startworkflowExchange")
    public Exchange startworkflowExchange(){

        return transExchage(ExchangeEnum.EXCHANGE_STARTWORKFLOW);
    }
    @Bean(name = "startworkflowBinding")
    public Binding startworkflowBinding(@Qualifier("startworkflowQueue") Queue queue,@Qualifier("startworkflowExchange") Exchange exchange) {

        return BindingBuilder.bind(queue).to(exchange).with(RountingKeyCons.STARTWORKFLOW).noargs();
    }

    /**
     * @descriotion 根据配置信息确定   exchange 类型
     * @param exchangeEnum  [exchangeEnum]
     * @return org.springframework.amqp.core.Exchange
     */
    private Exchange transExchage(ExchangeEnum exchangeEnum) {

        AbstractExchange exchange = null;

        switch (exchangeEnum.getExchangeTypeEnum()){

            //直连模式
            case DIRECT:
                exchange = new DirectExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel());
                break;
            //广播模式:
            case FANOUT:
                exchange = new FanoutExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel());
                break;
            //通配符模式
            case TOPIC:
                exchange = new TopicExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel());
                break;
            case HEADERS:
                exchange = new HeadersExchange(exchangeEnum.getExchangeName(), exchangeEnum.isDurable(), exchangeEnum.isAutoDel(),exchangeEnum.getArguments());
                break;
        }
        exchange.setDelayed(exchangeEnum.isDelayed());

        return exchange;
    }

}

       因为要保证生产者发送的消息能够正确的到达队列,所以需要引入消息确认机制。 消息确认机制的实现有多种方式。

  • 配置文件 + 配置rabbitTemplate
# rabbitmq 配置
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest
  #虚拟host  可以不设置 默认host
  virtual-host: /
  # 确认消息是否发送到交换机      ---  打开的设置
  publisher-confirm-type: correlated
  # 确认消息是否发送到队列        --- 打开的设置
  publisher-returns: true
  template:
    # 消息发送失败返回到队列中,yml 文件中需要配置  publisher-returns: true
    mandatory: false


-------java 代码
@Slf4j
@Configuration
public class RabbitmqConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback{
    
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) throws IOException {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        rabbitTemplate.setMandatory(true);// 无论成功失败,都会确认信息
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
        return rabbitTemplate;
    }
    
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(ack){
            log.info("【confirm】消息发送成功");
        } else {
            String id = correlationData==null? null: correlationData.getId();
            log.error("【confirm】消息发送失败,相关数据:{},原因:{}", id, cause);
        }
    }
    
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.info("【ReturnCallback】消息:" + returnedMessage.getMessage());
        log.info("【ReturnCallback】回应码:" + returnedMessage.getReplyCode());
        log.info("【ReturnCallback】回应信息:" + returnedMessage.getReplyText());
        log.info("【ReturnCallback】交换机:" + returnedMessage.getExchange());
        log.info("【ReturnCallback】路由键:" + returnedMessage.getRoutingKey());
    }
}

       理论上来说,按照上述这种方式配置之后,就应该在 生产者发送消息到 exchange 时,触发confirm() 方法;在消息 通过routingKey 路由到指定的 queue 失败时,触发 returnedMessage() 方法。但是在我实际的实现当中,没有 成功!!! 后面我又测试了其他的几种方式,原理都是一样的,就是给rabbitTemlate 设置 setConfirmCallback()\setReturnsCallback(),不过在发送消息时,就是不生效。 这是为什么呀?经过打断点,去看rabbitTemplate 的属性,发现 connectionFactory 的setPublisherReturns()\setPublisherConfirmType()属性为空;这就可以解释原因了。难道说是配置文件没有生效?于是,我才用了接下来的方式,用java 类实现配置。

@Configuration
public class RabbitMqTemplateConfig {

    /** 日志服务 */
    public static final Logger log = LoggerFactory.getLogger(RabbitMqTemplateConfig.class);

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private int port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;
    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;
    @Value("${spring.rabbitmq.template.mandatory}")
    private boolean mandatory;
    @Value("${spring.rabbitmq.publisher-returns}")
    private boolean publisherReturns;
    @Value("${spring.rabbitmq.publisher-confirm-type}")
    private CachingConnectionFactory.ConfirmType publisherConfirmType;

    @Bean(name = "connectionFactory")
    public CachingConnectionFactory connectionFactory () {

        /** 以下  这段代码是为了 配置 set 中的参数   yml 配置文件没有生效 */
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();

        connectionFactory.setHost(host);
        connectionFactory.setPort(port);
        connectionFactory.setPassword(password);
        connectionFactory.setUsername(username);
        connectionFactory.setVirtualHost(virtualHost);
        connectionFactory.setPublisherReturns(publisherReturns);
        connectionFactory.setPublisherConfirmType(publisherConfirmType);

        return connectionFactory;
    }

    @Bean(name = "rabbitTemplate")
    public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") CachingConnectionFactory cachingConnectionFactory) {

        RabbitTemplate rabbitTemplate = new RabbitTemplate();

        rabbitTemplate.setConnectionFactory(cachingConnectionFactory);
        // 设置开启 mandatory 才能触发回调函数
        rabbitTemplate.setMandatory(true);

        /** 消息到达 exchange 做一次判断 */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {

            if(ack){
                log.info("消息成功发送,成功不做处理 "+ correlationData.getId());
            }else{
                log.info("消息发送失败:"+correlationData.getId() + ", 出现异常:"+cause);
            }
        });
        /** exchage 与 queue 的binding  做一次判断  到达不做处理  出错开始执行 */
        rabbitTemplate.setReturnsCallback(returnedMessage -> {

            log.info("被退回的消息为:{}", returnedMessage.getMessage());
            log.info("replyCode:{}", returnedMessage.getReplyCode());
            log.info("replyText:{}", returnedMessage.getReplyText());
            log.info("exchange:{}", returnedMessage.getExchange());
            log.info("routingKey:{}", returnedMessage.getRoutingKey());
        });
        return rabbitTemplate;
    }
}

这样就解决了,发送消息或者发送消息到queue 失败时 回调函数失效的问题。

??? 这里也存在一种可能,就是配置文件的指定可能出现了问题。


       上面所说的是生产者端的消息确认,其实为了保证消费端能够正确的消费消息,避免因为网络波动等原因造成消息丢失,在消费端也应该引入消费确认机制。