Springboot整合RabbitMQ可靠性传输

Springboot整合RabbitMQ可靠性传输(分布式事务部分实现方案)

开发环境:rabbitmq-server-3.6.10、otp_win64_19.3(erlang版本);

1.RabbitMQ安装问题

在安装过程中,可能遇到以下问题:

  1. 如果同时安装了activemq和rabbitmq,默认配置下会启动失败,这是由于activemq也同时支持多种协议:tcp、amqp、stomp、mqtt,其中amqp监听的端口和rabbitmq一样(5672),导致冲突,在activemq.xml注释掉即可;
    在这里插入图片描述
  2. 在rabbitmq的sbin目录下,命令行rabbitmqctl status报错nodedown,TCP connection succeed but Erlang distribution failed,解决办法:把C:\Windows下的.erlang.cookie文件同步到Administrator下
  3. 最后rabbitmq-plugins enable rabbitmq_management添加可视化插件,就可以访问管理界面localhost:15672,如果新增了自定义用户,注意查看Can access vitual hosts属性,否则项目启动会报socket closed
    Springboot整合RabbitMQ可靠性传输_第1张图片

2.Springboot如何使用RabbitMQ

2.1 生产者配置

  1. 添加依赖
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-amqp</artifactId>
		<version>2.0.3.RELEASE</version>
	</dependency>
  1. application.yml配置
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    publisher-confirms: true
    publisher-returns: true
    template:
      mandatory: true
    cache:
      channel:
        size: 100

其中publisher-confirms和publisher-returns是消息投递到MQ的回调,在代码中配置消息回调后的执行方案,mandatory设置为true表示routingkey找不到exchange绑定的queue就return给生产者,否则直接将消息丢弃。

  1. RabbitTemplate 消息可靠性传递
    利用RabbitTemplate的回调机制,首先为消息设置一个uuid,传入CorrelationData,标志当前消息id,这样可以在消息投递后的回调中拿到这个id,然后把消息存入数据库或redis,性能考虑建议redis,回调时,如果投递成功,则从redis删除消息,如果投递失败,则可以设置定时任务重新投递或人工解决。
        CorrelationData correlationData = new CorrelationData();
        String uuid = UUID.randomUUID().toString();
        correlationData.setId(uuid);
        redisTemplate.opsForValue().set(uuid, msg);
        rabbitTemplate.convertAndSend("exchange1", "routingKey1", msg, correlationData);
  1. rabbitmq相关配置类
@Configuration
public class RabbitmqConfiguration {

    @Bean
    public Queue queue() {
        return new Queue("queue1");
    }

    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("exchange1");
    }

    @Bean
    public Binding binding() {
        Binding binding = BindingBuilder.bind(queue()).to(directExchange()).with("routingKey1");
        return binding;
    }

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 设置消息可靠性传递
     * @param connectionFactory
     * @return
     */
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        //消息发送到交换器时回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                System.out.println("redis key: " + correlationData.getId());
                System.out.println(s);
                if(b) {
                    System.out.println("delete pre-msg in redis");
                    redisTemplate.delete(correlationData.getId());
                } else {
                    //可以设置定时任务
                    System.out.println("try to resend msg");
                }
            }
        });
//        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
//            if(ack) {
//                System.out.println("delete pre-msg in redis");
//            } else {
//                System.out.println(correlationData.toString());
//                System.out.println(s.toString());
//            }
//        });

        //消息发送到交换器,但无队列与交换器绑定时回调;
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println(message.toString());
                System.out.println(replyCode);
                System.out.println(replyText);
                System.out.println(exchange);
                System.out.println(routingKey);
            }
        });
//        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
//            System.out.println(message.toString());
//            System.out.println(replyCode);
//            System.out.println(replyText);
//            System.out.println(exchange);
//            System.out.println(routingKey);
//        });
        return rabbitTemplate;
    }

}

本例采用direct模式。通过routingkey(routingKey1)找到与交换机(exchange1)绑定的队列(queue1)。
RabbitTemplate配置可靠性传输,需设置为原型模式SCOPE_PROTOTYPE,由于每次消息投递回调对应不同的线程,如果采用单例,回调时无法确定对应的消息id。
rabbitTemplate.setConfirmCallback中匿名类方法实现confirm()参数:CorrelationData为消息标志,boolean b代表是否投递成功。Stirng s不详,反正打印为null。
rabbitTemplate.setReturnCallback消息发送到交换器,但无队列与交换器绑定时回调。

2.2 消费者配置

除了保障生产者的消息投递,还需要保障消费者的成功消费。

  1. application.yml配置rabbitmq监听方式,ack为消息确认机制(这里为手动确认),prefetch为一次消费数量,retry为消息重发机制
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 10
        retry:
          enabled: true
          max-attempts: 3
  1. 监听代码,这里涉及mq的推拉模型,设置了prefetch,意味着当前消息是由mq主动推送的。监听方法中Message参数的getMessageProperties()方法可获取相关配置信息,例如是否持久化、消费序列等,但是获取不到投递时传入的自定义uuid。(可能是我没有找到)
@Component
@RabbitListener(queues = "queue1")
public class RabbitmqListener {

    /**
     *
     * @throws Exception
     * 只有异常抛出时,才会触发消息重发机制
     */
    @RabbitHandler
    public void process(String msg, Channel channel, Message messages) throws Exception {
        try {
            System.out.println(msg);
            //消费序列,从1开始
            System.out.println(messages.getMessageProperties().getDeliveryTag());
            //MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=true, receivedExchange=exchange1, receivedRoutingKey=routingKey1, deliveryTag=3, consumerTag=amq.ctag-EetiB3hRZg_g2KVLKJSP2Q, consumerQueue=queue1]
            System.out.println(messages.getMessageProperties().toString());
            //null
            System.out.println(messages.getMessageProperties().getCorrelationId());
            int i = 1/0;
            channel.basicAck(messages.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            System.out.println("IO Exception");
            /**
             * param1:消息唯一ID,由MQ生成
             * param2:批量确认包括当前ID之前的所有消息
             * param3:是否回队
             */
//            channel.basicNack(messages.getMessageProperties().getDeliveryTag(), false, true);
            throw e;
        }
    }

}
  1. 只有异常抛出的时候才会触发消息重发机制,本例中故意设置一个除0异常做测试,重发3次后消息回队,下次启动后重新推送,当然也可以配置死信队列单独处理,但是要设置rabbitmq.listener.simple.default-requeue-rejected为false。同时,channel提供了一个basicNack方法,意思是消息回队,可以在异常时手动调用,回队后会由rabbittmq重新发送。

  2. 最后是要保障监听接口的幂等性,即防止消息重复消费,对于服务提供者或消费者而言,无论是HTTP还是MQ发送数据,都要做接口幂等的保证。常见的有保存标志信息的唯一ID,如果存在则丢弃。比如,如果是数据库操作,可以设置某个字段的唯一索引,或者使用悲观锁/乐观锁。

你可能感兴趣的:(Springboot)