RabbitMQ学习笔记

解决问题

  1. 降低业务耦合,
    开闭原则(扩展开放,修改关闭),
    健壮性(级联调用异常)。

  1. 提高业务性能。

级联调用需要等到每个具体业务返回,否则线程一直阻塞。比如支付服务每个具体业务耗时50ms,总耗时200多ms。对于通知服务和积分服务并不过度关注结果,完全可以从支付服务中独立出来,使用消息队列异步处理,进一步提高程序性能。

  1. 削峰填谷。

比如双十一、618支付请求的QPS可能一会高、一会低。这时支付业务只需要负责向消息队列发送消息就可以返回结果,并发性能有一定程度的降低,并且消息的消费者可以按照自己的节奏更加平缓的进行业务处理。能够提高整个系统的健壮性。

基本角色和流程

  1. 交换机
  2. 消息队列
  3. 虚拟主机(类似于数据库中的数据库,起到隔离业务场景的效果,比如不同的虚拟主机中可以有相同的消息队列,避免冲突,类似于不同的数据库中可以有相同表名的表)。
  4. 消息发送者publisher
  5. 消息消费者consumer

RabbitMQ的基本架构
0c5d32852dad5e51ae90957fc834188.png

SpringAMQP

Spring统一实现了AMQP协议,提供了更便捷的API接口

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>
spring:
  rabbitmq:
    host: 47.107.63.143
    port: 5672
    username: admin
    password: password
    virtual-host: admin_vh
  • 发送消息 RabbitTemplate
@SpringBootTest
class PublisherApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void testSendMessage() {
        // work模型
        for (int i = 0; i < 50; i++) {
            rabbitTemplate.convertAndSend("work.queue", "work message,这是来自publisher的消息__" + i);
        }
    }

}
  • 接收消息 RabbitListener
@Component
@Slf4j
public class Consumer {
    @RabbitListener(queues = "work.queue")
    public void receive0(String message) throws InterruptedException {
        log.info("接收到消息: {}", message);
        Thread.sleep(20);
    }


    @RabbitListener(queues = "work.queue")
    public void receive1(String message) throws InterruptedException {
        log.warn("接收到消息: {}", message);
        Thread.sleep(200);
    }

    @RabbitListener(queues = "work.queue")
    public void receive2(String message) throws InterruptedException {
        log.error("接收到消息: {}", message);
        Thread.sleep(2000);
    }
}

work模型

默认情况下,一个队列可以被多个消费者监听,但是每条消息只会被其中一个消费者消费,默认情况下,消息按照顺序分配的方式分发给每个消息者。
如果希望消费者结合自身算力在处理完消息后接着处理,可以通过配置来进行实现,实现消费者的“能者多劳”:

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1	 # 当前消费者处理结束消息后,接着获取下一条消息处理

交换机

fanout交换机

fanout交换机忽略了路由关键字,能实现消息广播,该交换机会将消息广播到所有绑定的队列,比较适用于通告、广播的场景。
image.pngs

direct交换机

按照消息发送时指定的routingKey将其转发到交换机通过bindingKey绑定的队列中。
image.png

topic交换机

与direct交换机类似,但是topic交换机绑定队列时的bindKey可以使用通配符,更加灵活。bindKey以 【.】 进行分隔。

  • #: 匹配0格或多个字母
  • *: 匹配单个字符

image.png

direct/topic比较

  • direct交换机的bindKey只能时固定的值,而topic交换机的bindKey可以是福哦个单词通过【.】分隔后设置通配符,相比之下,topic交换机更加灵活。

Spring AMQP声明队列和交换机

交换机和队列的声明一般在消息的消费方进行声明,消息的生产者只需要负责向交换机中投递消息即可。在程序中声明交换机和队列可以避免不同环境下重复在MQ的控制台配置的问题.

Bean声明方式

@Configuration
public class FanoutExchangeConfig {
    // 通过Bean对象声明交换机和队列的基本步骤
    /*
     * 1. 声明交换机
     * 2. 声明队列
     * 3. 绑定交换机和队列
     * */
    @Bean
    public FanoutExchange declareFanoutExchange() {
        return ExchangeBuilder.fanoutExchange("admin.fanout").build();
    }

    @Bean
    public Queue declareFanoutQueue1() {
        return QueueBuilder.durable("fanout.queue1").build();
    }

    // 第一种绑定方式
    @Bean
    public Binding bindingFanoutQueue1ToExchange() {
        // 在进行下面的两个方法调用时,本质上并不是重新执行了一遍
        // Spring对标注了Bean对象的方法进行了动态代理,
        // 在手动进行方法调用时,会首先检查Spring中是否有方法返回的Bean对象
        // 如果有,则直接返回这个Bean对象,而不会重新调用方法执行方法逻辑返回一个新的Bean对象
        return BindingBuilder.bind(declareFanoutQueue1()).to(declareFanoutExchange());
    }

    // 第二中绑定方式,Bean对象参数注入的方式
    @Bean
    public Binding bindingFanoutQueue1ToExchange(FanoutExchange descFanoutExchange, Queue declareFanoutQueue1) {
        return BindingBuilder.bind(declareFanoutQueue1).to(descFanoutExchange);
    }
}

注解声明方式

  • 基于@RabbitListener注解的bindings属性实现
/*
* 声明了一个交换机:admin.direct,交换机类型是:direct
* 声明了一个队列:direct.queue1
* 队列绑定到交换机:direct.queue1 --> admin.direct
* 队列的bindingKey是:news、weather
*/
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1"),
        exchange = @Exchange(name = "admin.direct", type = ExchangeTypes.DIRECT),
        key = {"news", "weather"}
))
@Component
@Slf4j
public class Consumer {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "admin.direct", type = ExchangeTypes.DIRECT),
            key = {"news", "weather"}
    ))
    public void receive0(String message) throws InterruptedException {
        log.info("接收到消息: {}", message);
        Thread.sleep(20);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "admin.direct", type = ExchangeTypes.DIRECT),
            key = {"news", "daily"}
    ))
    public void receive1(String message) throws InterruptedException {
        log.warn("接收到消息: {}", message);
        Thread.sleep(200);
    }

}


  • 声明结果

image.png

消息转换器

当消费者向队列中添加实体消息时,会抛出异常,提示没有合适的消息转换器:

java.lang.IllegalArgumentException: SimpleMessageConverter only supports String, byte[] and Serializable payloads, received: com.example.publisher.User

	at org.springframework.amqp.support.converter.SimpleMessageConverter.createMessage(SimpleMessageConverter.java:164)
	at org.springframework.amqp.support.converter.AbstractMessageConverter.createMessage(AbstractMessageConverter.java:88)
	at org.springframework.amqp.support.converter.AbstractMessageConverter.toMessage(AbstractMessageConverter.java:70)
	at org.springframework.amqp.support.converter.AbstractMessageConverter.toMessage(AbstractMessageConverter.java:58)
	at org.springframework.amqp.rabbit.core.RabbitTemplate.convertMessageIfNecessary(RabbitTemplate.java:1831)
	at org.springframework.amqp.rabbit.core.RabbitTemplate.convertAndSend(RabbitTemplate.java:1137)
	at org.springframework.amqp.rabbit.core.RabbitTemplate.convertAndSend(RabbitTemplate.java:1130)

这是我们就需要手动配置一个用户序列化实体对象的消息转换器,这里使用:

  <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
  </dependency>

接着向Bean容器中注册序列化器对象,生产者和消费者都要添加:

@SpringBootApplication
public class PublisherApplication {

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

    @Bean
    public MessageConverter jackson2JsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

消息的可靠性

RabbitMQ消息的可靠性可以通过以下三种角色来共同保证:

  • 生产者推送消息的可靠性
  • RabbitMQ消息持久化
  • 消费者消费消息的可靠性

生产者推送消息的可靠性

生产者重连

使用Spring AMQP时可以通过配置:

spring:
  rabbitmq:
    connection-timeout: 1s # 连接的超时时间
    template:
      retry:
        enabled: true
        initial-interval: 1000ms # 连接失败后的等待时间
        multiplier: 1  # 连接失败后等待时长倍数:下次等待时长=等待时长倍数 * 初始等待时间(initial-interval)
        max-attempts: 3 # 最大尝试连接次数

生产者在重连时是阻塞式的,会对业务性能造成影响,因此一般不开启重连机制,当然也可以考虑异步线程发送消息的方式开启生产者重连。

生产者确认

  1. RabbitMQ提供了两种回调确认方案,分别是Puslisher-Confirm和Publisher-Return。MQ收到消息后,会通过回调的方式告知发送者消息的接收状态,比如:
  • 消息投递到了交换机,但是路由失败,此时MW会通过Publisher-Return回调消费者,返回ACK代表收到了消息,并告知异常原因。
  • 临时消息到达队列,返回ACK;持久化消息到达队列并完成持久化,返回ACK
  • 除去上述两种情况的异常消息状态,MQ会返回UACK代表消息异常,上述的三种方法都会经过Publisher-Confirm回调告知生产者,生产者收到回调后可以根据MQ返回的确认状态做出后续处理比如消息的重发或者日志统计。

【补充】:因为回调是基于网络IO进行的,因此会对MQ和业务系统的性能产生不同程度的影响。此外,Publisher-Return回调往往是因为业务方面路由配置出错导致的,因此为了系统的整体性能,一般不开启生产者确认机制,除非有对消息可靠性要求十分严格的业务需求出现时。

  1. 开启生产者确认
spring:
  rabbitmq:
    # 生产者确认配置
    publisher-returns: true	# 开启Publisher-Return回调
    # 确认模式:
    #   correlated:发送消息后异步等待broker的响应
    #   simple:发送消息同步等待broker的响应
    publisher-confirm-type: correlated	# 开启Publisher-Confirm回调
  • 对于Publisher-Return回调,需要统一配置:
@Configuration
@Slf4j
public class RabbitMQCommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnsCallback((returned) ->
                log.warn("消息路由失败,消息:{},路由键:{},应答码:{},原因:{}",
                        returned.getMessage(),
                        returned.getRoutingKey(),
                        returned.getReplyCode(),
                        returned.getReplyText()));
    }
}
/*
* 实例回调输出:消息路由失败,消息:(Body:'{"name":"测试姓名","age":88}' MessageProperties [headers={spring_returned_message_correlation=847b9d73-669f-40f9-8552-d39bc5ee4b05, __TypeId__=com.example.publisher.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]),路由键:daily123,应答码:312,原因:NO_ROUTE
*/
  • 对于Publisher-Confirm回调,需要在消息发送时针对消息指定:
@Test
void testSendMessage() throws InterruptedException {
    CorrelationData cd = new CorrelationData(UUID.randomUUID().toString()); // 在这里制定一个唯一ID,让RabbitMQ知道是哪个消息的回调
    cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
        @Override
        public void onFailure(Throwable ex) {
            // 回调程序执行失败的情况,很少发生,一般较少关注
            log.error("回调程序执行失败", ex);
        }

        @Override
        public void onSuccess(CorrelationData.Confirm result) {
            if (result.isAck()) {
                // 消息发送成功
                log.info("消息发送成功,result-{}", result);
            } else {
                // 消息发送失败
                log.error("消息发送失败,原因是:{}", result.getReason());
            }
        }
    });
    rabbitTemplate.convertAndSend("admin.direct", "daily123", new User("测试姓名", 88), cd);
    Thread.sleep(2000);	// 这里加延迟是为了看到回调结果,否则测试方法执行完就结束了
}

RabbitMQ消息持久化

RabbitMQ消息持久化指的是将内存中的消息持久化到磁盘,防止服务宕机或者重启导致队列中原有消息丢失的机制。

数据持久化

数据持久化包含:

  • 交换机持久化
  • 队列持久化
  • 消息持久化

一般多使用Spring AMQP来完成与RabbitMQ的交互,默认情况下,Spring AMQP声明的交换机和队列以及发送的消息都是持久化的,(也可以在控制台创建队列或者交换机时指定Durability为Durable进行持久化)比如通过添加配置参数完成队列中消息持久化:

delivery_mode = 2

Lazy Queue惰性队列

自RabbitMQ3.6版本开始,新增的一种持久化机制,自3.12后强制使用的一种持久化机制。如果是3.12之前的版本,可以在声明队列时通过指定下述参数使其成为惰性队列:

x-queue-mode = lazy

亦或在Java注解中通过argument参数声明:

@Component
@Slf4j
public class Consumer {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "admin.direct", type = ExchangeTypes.DIRECT),
            arguments = @Argument(name = "x-queue-mode", value = "lazy"),
            key = {"news", "daily"}
    ))
    public void receive1(User message) throws InterruptedException {
        log.warn("接收到消息: {}", message);
        Thread.sleep(200);
    }

}

或者Bean对象声明时:

@Configuration
public class FanoutExchangeConfig {
    @Bean
    public Queue declareFanoutQueue1() {
        return QueueBuilder.durable("fanout.queue1")
                                .lazy()	 // 开启惰性队列
                                .build();
    }
}

在这种队列会将收到的消息直接持久化到磁盘当中,并且只有在消费者订阅处理消息时,才会将消息加载到内存当中。

消费者消费消息的可靠性

消息确认

当消费者消息处理结束后,会向RabbitMQ发送一个回执告诉消息的处理结果。处理结果包含以下三种状态:

  • ack:消息被确认消费,无异常
  • uack:消息未被正常消费,RabbitMQ可再次选择投递消息。
  • reject:消息被拒绝,往往由于消息自身异常导致,RabbitMQ会将这条消息从队列中移除。

Spring AMQP实现了消息的确认功能,并可通过下述配置项指定消息的确认方式:

  • auto:自动确认(AOP切面环绕增强实现),消费者执行结束无异常抛出返回ack,抛出RuntimeException时返回nack,抛出MessageConvertException时返回reject
  • none:不处理。消息投递给消息者后立即从队列中删除
  • manual:手动回执确认
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto

失败重试

  1. 失败重试机制
    默认情况下,处理失败的消息如果是nack的会requeue重新回到队列然后重新投递给消费者,如果消费者还是无法正常处理,那么RabbitMQ重复的无效的消息投递会带来不必要的性能损耗,可以开启本地重试机制并配置重试次数来解决上述问题,如果本地重试次数耗尽,可以结合失败消息处理策略进行后续处理。可通过下述配置开启本地retry机制:
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto
        retry:
          enabled: true
          max-attempts: 3 # 最大尝试次数
          initial-interval: 1000ms  # 失败后重试的等待时间
          multiplier: 1 # 下次重试等待倍数
          stateless: true # 如果业务不包含事务,则为无状态,这里配置true;否则配置false

如果本地重试次数耗尽,则会触发失败消息处理策略。
2. 失败消息处理策略
通过MessageRecoverer接口进行处理,包含以下三种实现:

  • RejectAndDontRequeueRecoverer:返回reject拒绝消息,丢弃消息。默认方式
  • ImmediateRequeueMessageRecoverer:返回nack,消息重新入队
  • RepublishMessageRecoverer:返回ack,将消息投递到指定的交换机,比如死信交换机

【补充】:在控制台中可以通过x-dead-letter-exchage为某个队列配置死信交换机。

  • SpringAMQP指定失败消息重试策略:
@Configuration
@Slf4j
public class ErrorExchangeConfig {
    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }



    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "error.queue"),
            exchange = @Exchange(name = "error.direct", type = ExchangeTypes.DIRECT),
            key = {"error"}
    ))
    public void handleErrorMsg(User message) {
        // 这里监听的是error.queue队列中的消息
        log.error("error.queue接收到消息:{}", message);
    }
}

业务幂等

幂等指的是同一个业务执行一次或者多次,对业务状态的影响是相同的。这里指的是一个消息被多次消费后同消息倍第一次消费对业务产生的影响是相同的。

可以通过下述方式确保消息的幂等性:

  1. Redis-SETNX命令
    为每个消息设置一个唯一ID,在消费者获取到消息后可以将包含消息唯一ID在内的字符串当做KEY值,通过Redis的SETNX是否能够设置成功,判断消息是否被消费过从而确保幂等性。

死信队列

死信队列通常用来接收被消费者拒绝的消息、被消费者标注为NACK的消息,过期的消息或者队列满后再投递溢出的消息。在MQ中,可以通过:

  • “x-dead-letter-exchange”为某个队列绑定对应的死信交换机,
  • “x-dead-letter-routing-key”为死信队列指定key值

之后再通过为死信交换机绑定队列来实现死信队列。

在业务场景中,可以为死信队列添加监听器订阅处理该队列,对无法正确处理的消息进行日志记录或者预警通知,也可统计后做系统的错误分析,更方便的排查问题。

目前发现在SpringAMQP中只能通过@Bean的方式在声明普通队列时为其添加死信队列:

@Component
@Slf4j
public class Consumer {
    @Bean
    public Queue directQueue1() {
        return QueueBuilder.durable("direct.queue1")
                .withArgument("x-dead-letter-exchange", "dead.direct")	// 为direct.queue1指定死信交换机
                .withArgument("x-dead-letter-routing-key", "dead.letter")	// 为direct.queue1配置的私信交换机指定路由key
                .build();
    }

    @Bean
    public DirectExchange directExchange() {
        return ExchangeBuilder.directExchange("admin.direct")
                .build();
    }

    @Bean
    public Binding bindingDirectQueue1ToExchange(DirectExchange directExchange, org.springframework.amqp.core.Queue directQueue1) {
        return BindingBuilder.bind(directQueue1)
                .to(directExchange)
                .with("news");
    }


    @Bean
    public Binding bindingDirectQueue1ToExchange2(DirectExchange directExchange, org.springframework.amqp.core.Queue directQueue1) {
        return BindingBuilder.bind(directQueue1)
                .to(directExchange)
                .with("weather");
    }

    // 声明死信队列和死信交换机,并通过key=dead.letter绑定
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dead.queue"),
            exchange = @Exchange(name = "dead.direct", type = ExchangeTypes.DIRECT),
            arguments = @Argument(name = "x-queue-mode", value = "lazy"),
            key = {"dead.letter"}
    ))
    public void receive2(User message) {
        log.error("死信队列投递消息: {}", message);
    }

}

延迟队列

在延迟队列中的消息会在延迟时间到达后投递给订阅者进行处理,比较适用于订单超时的场景。延迟队列有以下两种实现方式:

  1. 通过为消息设置过期时间和配置死信队列的方式实现

  1. 通过延迟交换机插件实现
  • 下载插件

GitHub - rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ

  • 将插件放置到RabbitMQ的插件目录下
# 查看插件目录
rabbitmq-plugins directories -s
  • 启动插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  • 重启RabbitMQ
  • 在声明交换机时设置交换机的delayed属性为true
// 注解声明
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue",durable = "true"),
        exchange = @Exchange(name = "delay.direct", type = ExchangeTypes.DIRECT, delayed = "true"),
        key = {"delay.letter"}
))
public void receiveDelayMsg(User message) {
    log.error("延迟队列投递消息: {}", message);
}

// Bean对象声明
@Bean
public DirectExchange directExchange() {
    return ExchangeBuilder.directExchange("delay.direct")
            .delayed()	// 设置交换机为延迟交换机
            .build();
}
  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小s的s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值