SpringBoot2.x整合RabbitMQ

SpringBoot2.x整合RabbitMQ

依赖

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

yml配置

spring:
  rabbitmq:
    host: 192.168.142.210
    username: guest
    password: guest
    port: 5672
    virtual-host: /
    #消息发送确认回调
    publisher-confirms: true
    #发送返回监听回调
    publisher-returns: true
    #消费者确认--手动ack模式
    listener:
      simple:
        acknowledge-mode: manual
    #设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
    template:
      mandatory: true

配置类

设置了消息消费失败重试次数、失败消息的处理方式以及成功或失败的回调方法

@Configuration
@Slf4j
public class RabbitMQConfig {

    /**
     * 配置消息监听对象
     * 
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {

        log.info("自动配置RabbitListenerContainerFactory");
        SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory = new SimpleRabbitListenerContainerFactory();
        simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
        // 消费者数量
        simpleRabbitListenerContainerFactory.setConcurrentConsumers(5);
        // 最大消费者数量
        simpleRabbitListenerContainerFactory.setMaxConcurrentConsumers(10);
        // 手动ack,当消息n次重试消费端依旧处理失败时,可重新回归队列
        // 重启项目后可再次消费,但是如果未及时处理会导致消息队列中数据越来越多
        // simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);

        // 自动ack,若是n次重试仍旧消费失败后,消息会丢弃,
        // 虽然会产生数据丢失,但是可以防止消息队列中数据冗余
        simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.AUTO);

        // 消息转换
        simpleRabbitListenerContainerFactory.setMessageConverter(new Jackson2JsonMessageConverter());
        // 消费者自动启动
        simpleRabbitListenerContainerFactory.setAutoStartup(true);
        // 消费者每次从队列获取的消息数量
        simpleRabbitListenerContainerFactory.setPrefetchCount(1);

        // 重试机制
        // 这种方式不生效
        // simpleRabbitListenerContainerFactory.setRetryTemplate(retryTemplate());
        simpleRabbitListenerContainerFactory.setAdviceChain(
                RetryInterceptorBuilder
                        .stateless()
                        .recoverer(new RejectAndDontRequeueRecoverer())
                        .retryOperations(retryTemplate())
                        .build());

        // true时消费者消费失败,自动重新入队;
        // 重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
        simpleRabbitListenerContainerFactory.setDefaultRequeueRejected(true);

        return simpleRabbitListenerContainerFactory;

    }

    /**
     * 构造RabbitTemplate对象
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());

        // 必须设置为 true,不然当发送到交换器成功,但是没有匹配的队列,不会触发 ReturnCallback 回调
        // 而且 ReturnCallback 比 ConfirmCallback 先回调,意思就是 ReturnCallback 执行完了才会执行 ConfirmCallback
        rabbitTemplate.setMandatory(true);

        //指定 ConfirmCallback 回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            // 如果发送到交换器都没有成功(比如说删除了交换器),ack 返回值为 false
            // 如果发送到交换器成功,但是没有匹配的队列(比如说取消了绑定),ack 返回值为还是 true (这是一个坑,需要注意)
            if (ack) {
                if (null != correlationData) {
                    log.info("confirm回调方法>>>消息发送到交换机成功!回调消息ID为:{}", correlationData.getId());
                } else {
                    log.info("confirm回调方法>>>消息发送到交换机成功!");
                }
            } else {
                log.info("confirm回调方法>>>消息发送到交换机失败!,原因 : [{}]", cause);
            }
        });

        // 设置 ReturnCallback 回调   yml需要配置 publisher-returns: true
        // 如果发送到交换器成功,但是没有匹配的队列,就会触发这个回调
        rabbitTemplate.setReturnCallback((message, replyCode, replyText,
                                          exchange, routingKey) -> {
            log.info("returnedMessage回调方法,未匹配对列>>>" + new String(message.getBody(), StandardCharsets.UTF_8) + ",replyCode:" + replyCode
                    + ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey);
        });
        return rabbitTemplate;
    }

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        retryTemplate.setBackOffPolicy(backOffPolicy());
        retryTemplate.setRetryPolicy(retryPolicy());

        // 设置消费消息过程监听(不是必须)
        retryTemplate.registerListener(new RetryListener() {
            @Override
            public <T, E extends Throwable> boolean open(RetryContext retryContext, RetryCallback<T, E> retryCallback) {
                // 执行之前调用 (返回false时会终止执行)
                log.info("***********open: 开始 count: {}", retryContext.getRetryCount());
                return true;
            }

            @Override
            public <T, E extends Throwable> void close(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
                // 重试结束或消费成功的时候调用
                log.info("***********close: 结束: count: {} ", retryContext.getRetryCount());
            }

            @Override
            public <T, E extends Throwable> void onError(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
                //  异常 都会调用
                log.error("***********尝试第{}次重新调用", retryContext.getRetryCount());
            }
        });
        return retryTemplate;
    }

    /**
     * 消费时便重试时间间隔
     *
     * @return
     */
    @Bean
    public ExponentialBackOffPolicy backOffPolicy() {
        ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
        backOffPolicy.setInitialInterval(3000);
        backOffPolicy.setMaxInterval(10000);
        return backOffPolicy;
    }

    /**
     * 消费失败重试次数
     *
     * @return
     */
    @Bean
    public SimpleRetryPolicy retryPolicy() {
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        // 消息最大发送次数,包括第一次和重试次数
        retryPolicy.setMaxAttempts(3);
        return retryPolicy;
    }
}

创建队列绑定交换机

这里交换机和队列要设置持久化为true,防止宕机后消息丢失

public final static String USER_LOG = "user.log";

public final static String EXCHANGE = "exchange";

@Bean
public Queue userLogQueue() {
    /*
        durable:队列是否可持久化,默认为true。
        exclusive: 队列是否具有排它性,默认为false。
        autoDelete:队列没有任何订阅的消费者时是否自动删除,默认为false。
        */
    return new Queue(USER_LOG, true, false, false);
}

@Bean(EXCHANGE)
public TopicExchange topicExchange() {
	/*
      交换机持久化且不自动删除
    */
    return new TopicExchange(EXCHANGE,true,false);
}

@Bean
public Binding bindingUserLog(Queue userLogQueue, TopicExchange topicExchange) {
    return BindingBuilder.bind(userLogQueue).to(topicExchange).with(USER_LOG);
}

消息发送抽象类

@Slf4j
public abstract class BaseSender {

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 子类重写方法(业务方法调用此方法时可不指定交换机及路由键,由子类方法指定)
     *
     * @param object
     */
    public abstract void sendMessage(Object object);

    /**
     * 通用消息发送方法
     *
     * @param object       发送的对象
     * @param exchangeName 交换机名称
     * @param bindingKey   绑定路由key
     */
    void push(Object object, String exchangeName, String bindingKey) {
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        log.info("【Producer】发送的消息ID = {},消息:{}", correlationData.getId(), new Gson().toJson(object));
        try {
            rabbitTemplate.convertAndSend(exchangeName, bindingKey, object, correlationData);
        } catch (Exception e) {
            log.error("发送消息失败,原因:{}", e.getMessage());
        }
    }

}

消息接收抽象类

@Slf4j
public abstract class BaseReceiver {

    /**
     * 子类重写方法,实现各自业务逻辑
     *
     * @param channel 信道
     * @param message 消息
     */
    protected abstract void receiveMessage(Channel channel, Message message);

    /**
     * 获取具体对象
     *
     * @param message 消息
     * @param clazz 需要转换的类
     * @param <T>
     * @return
     */
    <T> T getObjectByClass(Message message, Class<T> clazz) {
        String body = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info("【MQ: Consumer成功接收到消息,Class: {} 】>>> {}", clazz.getName(), body);
        return JSONObject.parseObject(body, clazz);
    }

     /**
     * 消息接收确认
     *
     * @param channel 信道
     * @param message 消息
     */
    void basicAck(Channel channel, Message message) {
        try {
            // 确认接收到消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

     /**
     * 异常手动处理方法,这是最开始自定义的三次重试并且手动ack的方案
     * 但是后面spring自己有提供的重试机制
     *
     * @param channel 信道
     * @param message 消息
     * @param e       异常
     */
    void handelException(Channel channel, Message message, Exception e) {
        //是否发生过异常,发生过异常拒绝接受消息
        if (message.getMessageProperties().getRedelivered()) {
            try {
                Map<String, Object> headers = message.getMessageProperties().getHeaders();
                Integer retryCount = (Integer) headers.get("retryCount");
                if (retryCount == null || retryCount <= 3) {
                    headers.put("retryCount", retryCount == null ? 1 : ++retryCount);
                    log.info("【Consumer】消息已经回滚过,重试次数: {} 接收消息 : {}", retryCount, new String(message.getBody()));
                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
                } else {
                    // 拒绝消息,并且不再重新进入队列
                    log.info("【Consumer】消息已经回滚过,拒绝接收消息 : {}", new String(message.getBody()));
                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
                }

            } catch (Exception e1) {
                e1.printStackTrace();
            }
        } else {
            log.info("【Consumer】消息即将返回队列重新处理 :{}", new String(message.getBody()));
            //设置消息重新回到队列处理
            // requeue表示是否重新回到队列,true重新入队
            try {
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        e.printStackTrace();
    }
}

实际应用

  • 生产者
// 用户日志生产者,业务代码中引入sender,直接调用sendMessage方法即可
@Component
public class UserLogSender extends BaseSender {

    @Override
    public void sendMessage(Object object) {
        push(object, RabbitMQConfig.EXCHANGE, RabbitMQConfig.USER_LOG);
    }
}
  • 消费者
    当消费者消费时,如果在ack之前抛出异常,则会根据你设定的重试次数,重新发送消息给消费者。如果n次重试后仍旧失败,会根据你之前设定的ack方式,决定这条消息是丢弃还是重新放入队列(如果队列没有被删除,则重启项目后再次被消费)
@Component
public class UserLogReceiver extends BaseReceiver {

    @Resource
    private IUserLogMongoService userLogMongoService;

    @Override
    @RabbitListener(queues = {RabbitMQConfig.USER_LOG})
    protected void receiveMessage(Channel channel, Message message) {

        MongoUserLog userLog = getObjectByClass(message, MongoUserLog.class);
        // 业务代码...

        // 若是配置的是手动ack
        // simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        // 则成功后需要调用下面的方法手动确认
        // basicAck(channel, message);

    }
}
  • 消费成功,可看到生产者这边发送成功,消费者也拿到了消息
    在这里插入图片描述
  • 消费失败
    在这里插入图片描述
    • MANUAL模式下调用失败,三次重试后回到队列.
      在这里插入图片描述

    • AUTO模式下调用失败,三次重试后丢弃。可以看见total当时的一个峰值,最终归为0
      在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值