RabbitMQ - 基础(1)

RabbitMQ 四大核心概念

生产者:发送消息到的RabbitMQ客户端为生产者
交换机:是RabbitMQ非常重要的一个部件,用于接收来自生产者的消息,并且也需要将消息推送到队列中,交换机必须确切的知道如何处理接收到的消息,是将这些消息推送到指定队列还是多个队列,或是将消息丢弃都由交换机类型决定
队列:是RabbitMQ中存储消息数据结构,生产者生产的消息最终会存放在队列中,消费者消费消息需要从队列中获取
消费者:并且并且消费队列中的消息为消费者


在初始时,需要先创建队列,交换机
然后将队列和交换机绑定,如果交换机是Direct Exchange,则需要传入一个Binding Key,这个key是干嘛的呢?在生产者需要使用direct exchange向消息队列写入数据时,会指定一个routing key,在direct exchage中会将消息发送给所有bindingkey 与该routing key相同的queue,如果生产者发送消息时没有传来exchange(传来的为null或空字符串),则会使用默认交换机。
所有queue在创建时,都会和默认交换机绑定,并且binding key等于queue name。

使用编程式代码与RabbitMQ交互

首先导入maven

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.17.1</version>
</dependency>

首先创建一个ConnectionFactory类用于创建与RabbitMQ的Connection

public class MQConnectionFactory {
    public static Connection newConnection() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setPort(5672);
        factory.setHost("localhost");
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        return factory.newConnection();
    }

}

使用该静态方法得到的Connection对象,我们可以用来创建多个channel,(因为对于计算机来说,建立连接的开销是非常昂贵的,所以复用同一个连接来发送/接受数据)
然后可以通过得到的Channel对象创建Queue,Exchange,绑定Queue等操作

// 创建一个名为test-exchange的DirectExchange
channel.exchangeDeclare("test-exchange", BuiltinExchangeType.DIRECT);
// 创建一个名为test-queue的Queue
channel.queueDeclare("test-queue",false,false,false,null);
// 将刚刚创建的队列和交换机绑定,且BindingKey为red
channel.queueBind("test-queue","test-exchange","red");
// 最后,发送消息到这个交换机,并指定RoutingKey
channel.basicPublish("test-exchanges","red",null,"hello world".getBytes());

而在另一侧,如果消费者需要消费这个队列的消息,只需要调用
== channel.basicConsume ==方法

当调用Consumers相关的接口方法时,单个订阅始终由其消费者标签引用。消费者标签可以由客户端或者服务器来生成,用于消费者的身份识别。想让RabbitMQ生成一个节点范围内的唯一标签,可以使用不含有消费者标签属性的Channel#basicConsume 重载,或者传递一个空字符串做为消费者标签,然后使用Channel#basicConsume返回的值。消费者标签同样用于清除消费者之用。
不同的消费者实例必须持有不同的消费者标签。非常不建议在同一个连接上出现重复的消费者标签,这会导致 自动连接覆盖 问题,并在监控消费者时混淆监控数据。

    boolean autoAck = false;
    channel.basicConsume(queueName, autoAck, "myConsumerTag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             String routingKey = envelope.getRoutingKey();
             String contentType = properties.getContentType();
             long deliveryTag = envelope.getDeliveryTag();
             // (process the message components here ...)
             channel.basicAck(deliveryTag, false);
         }
     });

自动连接恢复问题

依据官网的解释:

客户端与RabbitMQ之间可能出现网络不通。RabbitMQ Java客户端支持自动恢复连接和拓扑(队列、交换、绑定和消费者)。
许多应用程序的自动恢复过程遵循以下步骤:

  • 重新连接
  • 恢复连接侦听器
  • 重新打开通道
  • 恢复通道侦听器
  • 恢复通道基本。Qos设置、发布者确认和事务设置
  • 拓扑恢复包括以下操作,针对每个通道执行
  • 重新声明交换(预定义的交换除外)
  • Re-declare队列
  • 恢复所有绑定
  • 恢复所有消费者
    从Java客户机的4.0.0版本开始,默认情况下启用了自动恢复(因此也启用了拓扑恢复)。

拓扑恢复依赖于实体(队列、交换、绑定、消费者)的每个连接缓存。例如,在连接上声明队列时,它将被添加到缓存中。当它被删除或计划删除时(例如,因为它是自动删除的),它将被删除。这个模型有一些限制,如下所述。

要禁用或启用自动连接恢复,请使用factory.setAutomaticRecoveryEnabled(boolean)方法。下面的代码片段显示了如何显式启用自动恢复(例如,对于4.0.0之前的Java客户端):

看完似乎还是一头雾水,大概意思是
当使用相同的消费者标签时,如果一个消费者连接断开了,另一个消费者使用相同的消费者标签重新连接,RabbitMQ 会认为它是同一个消费者,并覆盖原来的连接,这时假设原来的那个消费者还在消费这个消息,就会导致这条消息被重复消费。

在Spring Boot中配置RabbitMQ信息

@Configuration
public class RabbitConfiguration {
    @Bean
    public DirectExchange orderExchange(){
        return new DirectExchange("orderExchange");
    }

    @Bean
    public Queue orderQueue(){
        return new Queue("orderQueue",false,false,false);
    }

    @Bean
    public Binding orderQueueBindingOrderExchange(Queue orderQueue, DirectExchange orderExchange){
        return BindingBuilder.bind(orderQueue).to(orderExchange).with("orders");
    }
}

在这个例子中创建了一个名字为orderExchange的DirectExchange和一个名字为orderQueue的队列,并将它俩以binding key为orders绑定。
如果消费者需要监听某个队列并消费则更简单

@Component
public class OrderListener {
    @RabbitListener(queues = "orderQueue")
    public void consumer1(Object message){
        System.out.println("处理了一个消息" + message);
    }
}

但在此之前,需要在创建一个MessageConverter类用于序列化和反序列化对象

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(ConsumerApplication.class);
    }


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

如何发送消息呢?
只需要注入RabbitTemplate对象并调用它的convertAndSend方法即可

@RestController
public class OrderController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @GetMapping("/buy1")
    public String buy1(){
        rabbitTemplate.convertAndSend("broadExchange",null,"123");
        return "购买成功";
    }

    @GetMapping("/buy2")
    public String buy2(){
        rabbitTemplate.convertAndSend("broadExchange",null,"123");
        return "购买成功";
    }


    @GetMapping("/timeout")
    public String timeout(){
        rabbitTemplate.convertAndSend(null,"unpaidQueue","即将超时的订单");
        return "订单超时测试";
    }
}

死信队列

死信队列/交换机只是一种相较于普通队列/交换机增加了一些参数的队列/交换机
当一个队列的一些消息过期后(消息可以设置过期时间),如果没有给该队列配置消息过期后的去向,他就会被删除。
消息过期的几种情况

被拒绝
过期
超过标准(到达最大长度)

但是程序员可以通过给Queue配置一个死信交换机(x-dead-letter-exchange),RabbitMQ就会在当前Queue的消息过期后将这个消息交给配置的死信交换机(注:这个交换机的类型可以是任意),死信交换机又会将消息发给对应的队列,这个对应的队列则是由交换机类型和queue的x-dead-letter-routing-key决定。
整个过程用一句话概括就是消息过期后rabbitmq将消息由又进行了一次发送操作,发送的目标就是我们配置的Exchange和RoutingKey
在以下的这个例子中创建了一个队列,该队列的全部信息会在10(x-message-ttl)秒后过期,并由rabbitmq发送给名字为orderExchange的交换机,且routingkey为timeout

    @Bean
    public Queue unpaidQueue(){
        Queue unpaidQueue = new Queue("unpaidQueue", true, false, false);
        unpaidQueue.addArgument("x-message-ttl",10000);
        unpaidQueue.addArgument("x-dead-letter-exchange","orderExchange");
        unpaidQueue.addArgument("x-dead-letter-routing-key","timeout");
        return unpaidQueue;
    }

除了给队列一个超时时间使消息过期,也可以在发消息时通过构造一个Message对象并给他一个超时时间令其一段时间后过期。例:

    @GetMapping("/timeout")
    public String timeout(){
        MessageProperties properties = new MessageProperties();
        properties.setExpiration("10000");
        Message message = rabbitTemplate.getMessageConverter().toMessage("即将超时的订单", properties);

        rabbitTemplate.convertAndSend(null, "unpaidQueue", message);
        return "订单超时测试";
    }

这个例子中让该消息10秒后过期

通道和并发的注意事项(线程安全)

依经验而言,应该尽量避免在线程间共享通道对象。应用应该尽可能每个线程都使用单独的通道,而不是将通道共享给多个线程。
虽然可以安全地并发调用通道上的某些操作,但有些操作则不能并发调用,如果那样做会导致错误的帧交错在网络上,或造成重复确认等问题。
在共享的通道上并发执行发布会导致错误的帧交错在网络上,触发连接级别的协议异常并导致连接被代理直接关闭。因此,需要在应用程序代码中进行显式同步(必须在关键部分调用Channel#basicPublish)。线程之间共享通道也会干扰发布者确认。最好能够完全避免在共享的通道上上进行并发发布,例如通过每个线程使用一个通道的方式实现并发。
也可以通过通道池的方式来避免在共享通道上并发发布消息:一旦一个线程使用完了某个通道,就将通道归还到池中,使得通道可以被其他线程再次使用。通道池可以视为一个特殊的同步解决方案。建议使用现成的池库来实现,而不是自己实现。例如开箱即用的 Spring AMQP 。

取自RabbitMQ中文文档,所以如果用的是Spring AMQP就不用担心用RabbitMQ时的线程安全了,Spring已经通过通道池的方式,保证了我们在使用RabbitMQ中间件时的线程安全问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值