初识RabbitMQ(小袁の初体验)

RabbitMQ消息中间件

一、消息中间件介绍

  1. 我们用java来举例子, 打个比方 我们客户端发送一个下单请求给订单系统(order)订单系统发送了 一个请求给我们的库存系统告诉他需要更改库存了, 我已经下单了, 这里, 每一个请求我们都可以看作一条消息,但是 我们客户端需要等待订单系统告诉我这条消息的处理结果(我到底有没有下单成功) 但是 订单系统不需要知道库存系统这条消息的处理情况 因为无论你库存有没有改动成功, 我订单还是下了, 因为是先下完了订单(下成功了) 才去更改库存, 库存如果更改出BUG了 那是库存系统的问题, 这个BUG不会影响订单系统。如果这里你能理解的话, 那么我们就能发现 我们用户发送的这条消息(下订单),是需要同步的(我需要知道结果), 订单发送给库存的消息,是可以异步的(我不想知道你库存到底改了没,我只是通知你我这边成功下了一个订单)。

    那么如果我们还按原来的方式去实现这个需求的话, 那么结果会是这样:

    image-20200430132621164

    在实际使用的时候,我们也可以使用线程池处理我们的订单入库存的步骤,但是线程池会有线程池阻塞等缺点。

    我们可以采用如下设计方式实现异步存储入库操作。

    image-20200430132722108

二、RabbitMQ介绍以及AMQP协议

  1. AMQP 协议中的基本概念:

    Broker: 接收和分发消息的应用,我们在介绍消息中间件的时候所说的消息系统就是Message Broker。

    Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。

    Connection: publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。

    Channel: 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。

    Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。

    Queue: 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。

    Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。

    Exchange的类型

    direct :

    ​ 这种类型的交换机的路由规则是根据一个routingKey的标识,交换机通过一个routingKey与队列绑定 ,在生产者生产消息的时候 指定一个routingKey 当绑定的队列的routingKey 与生产者发送的一样 那么交换机会吧这个消息发送给对应的队列。

    fanout:

    ​ 这种类型的交换机路由规则很简单,只要与他绑定了的队列, 他就会吧消息发送给对应队列(与routingKey没关系)

    topic:

    ​ 这种类型的交换机路由规则也是和routingKey有关 只不过 topic他可以根据:星,#( 星号代表过滤一单词,#代表过滤后面所有单词, 用.隔开)来识别routingKey 我打个比方 假设 我绑定的routingKey 有队列A和B A的routingKey是:星.user B的routingKey是: #.user

    ​ 那么我生产一条消息routingKey 为: error.user 那么此时 2个队列都能接受到, 如果改为 topic.error.user 那么这时候 只有B能接受到了

    headers:

    ​ 这个类型的交换机很少用到,他的路由规则 与routingKey无关 而是通过判断header参数来识别的, 基本上没有应用场景,因为上面的三种类型已经能应付了。

  2. 主流MQ对比

    image-20200430133349866

三、Springboot整合RabbitMQ

引入依赖

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

配置连接、交换机、队列

 @Bean
public ConnectionFactory connectionFactory() {
    CachingConnectionFactory connectionFactory = new
CachingConnectionFactory("localhost",5672); //我这里直接在构造方法传入了
// connectionFactory.setHost();
// connectionFactory.setPort(); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); connectionFactory.setVirtualHost("testhost"); 
  //是否开启消息确认机制
  //connectionFactory.setPublisherConfirms(true);
  return connectionFactory;
}

创建交换机并绑定队列

 @Bean
public DirectExchange  defaultExchange() {
    return new DirectExchange("directExchange");
}
@Bean
public Queue queue() { //名字 是否持久化
    return new Queue("testQueue", true);
}
@Bean
public Binding binding() {
//绑定一个队列 to: 绑定到哪个交换机上面 with:绑定的路由建(routingKey)
return BindingBuilder.bind(queue()).to(defaultExchange()).with("direct.key");
}

发送消息模板

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
//注意 这个ConnectionFactory 是使用javaconfig方式配置连接的时候才需要传入的 如果是yml配置的连接的话是不需要的
RabbitTemplate template = new RabbitTemplate(connectionFactory);
    return template;
}
@Component
public class TestSend {
    @Autowired
    RabbitTemplate rabbitTemplate;
public void testSend() {
//至于为什么调用这个API 后面会解释
//参数介绍: 交换机名字,路由建, 消息内容 		  
  rabbitTemplate.convertAndSend("directExchange", "direct.key", "hello");
} 
}

接收消息

@Component
public class TestListener  {
 @RabbitListener(queues = "testQueue")
    public void get(String message) throws Exception{
        System.out.println(message);
    }
}

四、可靠性保证

  1. 生产者

    不确定消息有没有发送到Broker,可以采用失败回调,发送方确认模式

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); //开启mandatory模式(开启失败回调)
    template.setMandatory(true);
    //指定失败回调接口的实现类 
                                                                               template.setReturnCallback(new MyReturnCallback()); return template;
    }
    
    public class MyReturnCallback implements RabbitTemplate.ReturnCallback {
    @Override
     public void returnedMessage(Message message, int replyCode, String replyText,String exchange, String routingKey) {
    } }
    System.out.println(message);
    System.out.println(replyCode);
    System.out.println(replyText);
    System.out.println(exchange);
    System.out.println(routingKey);
    }
    }
    
    

    当指定的交换机不能吧消息路由到队列时(没有指定路由建或者指定的RouteKey没有绑定对应的队列或者压根就没有绑定队列都会失败) 消息就会发送失败

    //开启发送确认模式	
    connectionFactory.setPublisherConfirms(true);
    
    
    //在rabbitmqTemplate配置一下
    template.setConfirmCallback(new MyConfirmCallback());
    
    //失败回调
     public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback{
    @Override
       //业务id,是否发送成功,原因
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            System.out.println(correlationData);
            System.out.println(ack);
           System.out.println(cause);
    } }
    

    为了进一步保证我们交换机的可靠性,我们可以定义一个备用交换机进行绑定主交换机,当主交换机不可用时,会自动进入备用交换机,一般情况备用交换机都是采用fanout类型。

    @Bean
    public DirectExchange defaultExchange() {
    Map<String, Object> map = new HashMap<>();
     //备用交换机名称
     map.put("alternate-exchange", "name");
     return new DirectExchange("directExchangeTest4", false, false, map);
    }
    
  2. 消费者

    手动确认

    为什么要确认消费? 默认情况下消费者在拿到rabbitmq的消息时 已经自动确认这条消息已经消费了, 讲白话就 是rabbitmq的队列里就会删除这条消息了, 但是 我们实际开发中 难免会遇到这种情况, 比如说 拿到这条消息发现我处理不了比如说参数不对, 又比如说 我当前这个系统出问题了, 暂时不能处理这个消息, 但是这个消息已 经被你消费掉了 rabbitmq的队列里也删除掉了, 你自己这边又处理不了, 那么这个消息就被遗弃了。 这种情 况在实际开发中是不合理的, rabbitmq提供了解决这个问题的方案, 也就是我们上面所说的confirm模式 只是我 们刚刚讲的是发送方的 这次我们来讲消费方的。

    配置手动确认:

     @Bean
    public SimpleRabbitListenerContainerFactory
    simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
                new SimpleRabbitListenerContainerFactory();
    //这个connectionFactory就是我们自己配置的连接工厂直接注入进来 simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory); 
      //这边设置消息确认方式由自动确认变为手动确认 simp
    

    消费者手动确认:

    @Component
    public class TestListener  {
    //containerFactory:指定我们刚刚配置的容器 @RabbitListener(queues = "testQueue",containerFactory =
    "simpleRabbitListenerContainerFactory")
        public void getMessage(Message message, Channel channel) throws Exception{
    System.out.println(new String(message.getBody(),"UTF-8")); 
      System.out.println(message.getBody());
    //这里我们调用了一个下单方法 如果下单成功了 那么这条消息就可以确认被消费了,即实际业务处理类 
      boolean f =placeAnOrder();
    if (f){
    //传入这条消息的标识, 这个标识由rabbitmq来维护 我们只需要从message中拿出来就可以 
      //第二个boolean参数指定是不是批量处理的 什么是批量处理我们待会儿会讲到 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }else {
    //当然 如果这个订单处理失败了 我们也需要告诉rabbitmq 告诉他这条消息处理失败了 可以退回
    也可以遗弃 要注意的是 无论这条消息成功与否 一定要通知 就算失败了 如果不通知的话 rabbitmq端会显示这条 消息一直处于未确认状态,那么这条消息就会一直堆积在rabbitmq端 除非与rabbitmq断开连接 那么他就会把这条 消息重新发给别人 所以 一定要记得通知!
    //前两个参数 和上面的意义一样, 最后一个参数 就是这条消息是返回到原队列 还是这条消息作废 就是不退回了。
    channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
    //其实 这个API也可以去告诉rabbitmq这条消息失败了 与basicNack不同之处 就是 他不能批量
    处理消息结果 只能处理单条消息 其实basicNack作为basicReject的扩展开发出来的
     //channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
    } }
    

    消息预取

    rabbitmq 默认 他会最快 以轮询的机制吧队列所有的消息发送给所有客户端

    那么 这种机制会有什么问题呢, 对于Rabbitmq来讲 这样子能最快速的使自己不会囤积消息而对性能造成影响, 但是 对于我们整个系统来讲, 这种机制会带来很多问题, 比如说 我一个队列有2个人同时在消费,而且他们处理 能力不同, 我打个最简单的比方 有100个订单消息需要处理(消费) 现在有消费者A 和消费者B , 消费者A消费一 条消息的速度是 10ms 消费者B 消费一条消息的速度是15ms ( 当然 这里只是打比方) 那么 rabbitmq 会默认给 消费者A B 一人50条消息让他们消费 但是 消费者A 他500ms 就可以消费完所有的消息 并且处于空闲状态 而 消费 者B需要750ms 才能消费完 如果从性能上来考虑的话 这100条消息消费完的时间一共是750ms(因为2个人同时在 消费) 但是如果 在消费者A消费完的时候 能把这个空闲的性能用来和B一起消费剩下的信息的话, 那么这处理速 度就会快非常多。

    配置消息预取:

    //设置消息预取的数量1-2500,500平均值
    simpleRabbitListenerContainerFactory.setPrefetchCount(1);
    

    死信交换机

    在创建队列的时候 可以给这个队列附带一个交换机, 那么这个队列作废的消息就会被重 新发到附带的交换机,然后让这个交换机重新路由这条消息

     @Bean
    public Queue queue() {
    Map<String,Object> map = new HashMap<>();
    //设置消息的过期时间 单位毫秒
    map.put("x-message-ttl",10000);
    //设置附带的死信交换机
    map.put("x-dead-letter-exchange","exchange.dlx");
    //指定重定向的路由建 消息作废之后可以决定需不需要更改他的路由建 如果需要 就在这里指定 
     map.put("x-dead-letter-routing-key","dead.order");
    return new Queue("testQueue", true,false,false,map);
    }
    

    大致效果如图

    image-20200430144227781

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值