RabbitMQ基础篇

1、RabbitMQ安装

2、RabbitMQ介绍

官网

2007年,Rabbit技术公司基于AMQP开发了RabbitMQ 1.0。为什么要用Erlang语言呢?因为Erlang是作者Matthias擅长的开发语言。第二个就是Erlang是为电话交换机编写的语言,天生适合分布式和高并发。

为什么要取Rabbit Technologies这个名字呢?因为兔子跑得很快,而且繁殖起来很疯狂。

从最开始用在金融行业里面,现在RabbitMQ已经在世界各地的公司中遍地开花。国内的绝大部分大厂都在用RabbitMQ,包括头条,美团,滴滴(TMD),去哪儿,艺龙,淘宝也有用。

RabbitMQ和Spring家族属于同一家公司:Pivotal。

当然,除了AMQP之外,RabbitMQ支持多种协议,STOMP、MQTT、HTTP、WebSockets。

2.1、工作模型及组件

我们先从下面这张图开始学习

2.1.1、Broker

Broker中文件翻译是代理/中介,这里我们就可以看做是RabbitMQ服务器,默认端口5672。

2.1.2、Connection

无论是生产者发送消息,还是消费者接收消息,都必须要跟Broker之间建立一个连接,这个连接是一个TCP的长连接。

2.1.3、Channel

如果所有的生产者发送消息和消费者接收消息,都直接创建和释放TCP长连接的话,对于Broker来说肯定会造成很大的性能损耗,也会浪费时间。

所以在AMQP里面引入了Channel的概念,它是一个虚拟的连接。我们把它翻译成通道,或者消息信道。这样我们就可以在保持的TCP长连接里面去创建和释放Channel,大大了减少了资源消耗。

不同的Channel是相互隔离的,每个Channel都有自己的编号。对于每个客户端线程来说,Channel就没必要共享了,各自用自己的Channel。

另外一个需要注意的是,Channel是RabbitMQ原生API里面的最重要的编程接口,也就是说我们定义交换机、队列、绑定关系,发送消息,消费消息,调用的都是Channel接口上的方法。

2.1.4、Queue

Queue是RabbitMQ用来存储消息的对象。实际上RabbitMQ是用数据库来存储消息的,这个数据库跟RabbitMQ一样是用Erlang开发的,名字叫Mnesia。我们可以在磁盘上找到 Mnesia的存储路径。

C:\Users\用户名\AppData\RoamingRabbitMQ\db\rabbit@用户名-mnesia

队列也是生产者和消费者的纽带,生产者发送的消息到达队列,在队列中存储。消费者从队列消费消息。

2.1.5、Consumer

就是消费者的意思。RabbitMQ中提供了两种消费的模式

  • Pull模式,对用方法BasicGet
  • Push模式,对应方法BasicConsume

Pull模式,对应的方法是 basicGet。消息存放在服务端,只有消费者主动获取才能拿到消息。如果每隔一段时间获取一次消息,消息的实时性会降低。但是好处是可以根据自己的消费能力决定获取消息的频率。

Push模式,对应的方法是basicConsume,只要生产者发消息到服务器,就马上推送给消费者,消息保存在客户端,实时性很高,如果消费不过来有可能会造成消息积压。Spring AMQP是 push方式,通过事件机制对队列进行监听,只要有消息到达队列,就会触发消费消息的方法。

2.1.6、Exchange

Exchange是交换机的意思。其作用是将消息按照规则分发到Queue中。所以,Exchange和这些需要接收消息的队列必须建立一个绑定关系,并且为每个队列指定一个特殊的标识。

Exchange和队列是多对多的绑定关系,也就说,一个交换机的消息一个路由给多个队列,一个队列也可以接收来自多个交换机的消息。

绑定关系建立好之后,生产者发送消息到Exchange,也会携带一个特殊的标识。当这个标识跟绑定的标识匹配的时候,消息就会发给一个或者多个符合规则的队列。

2.1.7、Vhost

Vhost可以理解为虚拟主机,它和rabbitMQ的关系类似于,虚拟机和物理主机。同一个RabbitMQ服务器,可以创建多个Vhost,它们彼此是独立的,拥有自己的交换机、队列、绑定等,拥有自己的权限机制。我们安装RabbitMQ的时候会自带一个默认的VHOST,名字是“/”。

创建使用介绍

2.2、路由方式

RabbitMQ中一共有四种类型的交换机,DirectTopicFanoutHeaders。其中Headers 不常用。交换机的类型可以在创建的时候指定,网页或者代码中。

2.2.1、Direct直连

Direct Exchange

消息的routing keybinding key完全匹配,才能路由到某一个队列。

eg: channel.basicPublish("MY_DIRECT_EXCHANGE”," spring” ," msg1”)只有第一个队列能收到消息。

2.2.2、Topic主题

Topic Exchange

消息的routing keybinding key进行规则匹配,并且可以路由到多个队列

这里需要说明的路由规则如下

  • * 代表不多不少一个单词
  • #代表0个或者多个单词

eg.根据一下填写可以路由到的队列。

routing keyqueue
junior.abc.jvmJUNIOR_QUEUE
senior.nettyNETTY_QUEUE、SENIOR_QUEUE
2.2.3、Fanout广播

广播类型的交换机与队列绑定时,不需要指定绑定键。因此生产者发送消息到广播类型的交换机上,也不需要携带路由键。消息达到交换机时,所有与之绑定了的队列,都会收到相同的消息的副本。

egchannel.basicPublish(" MY_FANOUT_EXCHANGE","", "msg 4")三个队列都会收到msg 4。

2.3、延迟消息实现

2.3.1、场景

假设有一个业务场景:超过30分钟未付款的订单自动关闭,这个功能应该怎么实现?

方案有如下两种:

  • 利用RabbitMQ的**死信队列(Dead Letter Queue)**来实现。
  • 利用rabbitmq-delayed-message-exchange实现
2.3.2、利用RabbitMQ的死信队列来实现

利用消息的过期时间,过期之后投递到DLX(死信交换机),路由到DLQ(死信队列),监听DLQ,实现了延迟队列。

2.3.2.1、消息的流转流程:

生产者——>原交换机——>原队列(超过TTL之后)——>死信交换机——>死信队列——>最终消费者

死信消息流转图

2.3.2.2、消息过期设置

实现消息过期的设置有两种

1、设置队列属性x-message-ttl

设置了该属性,队列中所有的消息超过时间未被消费时,都会过期。不管是谁的包裹都一视同仁。

@Bean("ttlQueue")
public Queue queue() {
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("x-message-ttl", 11000); // 队列中的消息未被消费11秒后过期
    return new Queue("TTL_QUEUE", true, false, false, map);
}
2、设置消息属性

通过MessageProperties.setExpiration("4000")方法设置消息的过期时间。

MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("4000"); // 消息的过期属性,单位ms
Message message = new Message("这条消息4秒后过期".getBytes(), messageProperties);
rabbitTemplate.send("TTL_EXCHANGE", "test.ttl", message);

注意:如果两者都设置了过期时间,先到期,先生效。

2.3.2.3、死信会去哪里?

队列在创建的时候可以指定一个死信交换机 DLX (Dead Letter Exchange)。死信交换机绑定的队列被称为死信队列DLQ (Dead Letter Queue),DLX实际上也是普通的交换机,DLQ也是普通的队列(例如替补球员也是普通球员)。

image-20210714175909355

也就是说,如果消息过期了,队列指定了DLX,就会发送到DLX。如果DLX绑定了DLQ,就会路由到DLQ。路由到DLQ之后,我们就可以消费了。

2.3.2.4、死信队列如何使用?

下面我们通过一个例子来演示死信队列的使用。

  1. 声明原交换机(ORI_USE_EXCHANGE ) 、原队列(ORI_uSE_QUEUE ),相互绑定。指定原队列的死信交换机(DEAD_LETTER_EXCHANGE)。
  2. 声明死信交换机 (DEAD_LETTER_EXCHANGE)、死信队列(DEAD_LETTER_QUEUE),并且通过"#"绑定,代表无条件路由。
  3. 最终消费者监听死信队列,在这里面实现检查订单状态逻辑。
  4. 生产者发送消息测试,设置消息10秒过期。

代码链接

 // 指定队列的死信交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("x-dead-letter-exchange","GP_DEAD_LETTER_EXCHANGE");
// arguments.put("x-expires",9000L); // 设置队列的TTL
// arguments.put("x-max-length", 4); // 如果设置了队列的最大长度,超过长度时,先入队的消息会被发送到DLX

// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare("GP_ORI_USE_QUEUE", false, false, false, arguments);

// 声明死信交换机
channel.exchangeDeclare("GP_DEAD_LETTER_EXCHANGE","topic", false, false, false, null);
// 声明死信队列
channel.queueDeclare("GP_DEAD_LETTER_QUEUE", false, false, false, null);
// 绑定,此处 Dead letter routing key 设置为 #
channel.queueBind("GP_DEAD_LETTER_QUEUE","GP_DEAD_LETTER_EXCHANGE","#");
System.out.println(" Waiting for message....");
2.3.3、利用rabbitmq-delayed-message-exchange实现

使用死信队列实现延时消息的缺点:

  • 如果统一用队列来设置消息的TTL,当梯度非常多的情况下,比如1分钟,2分钟,5分钟,10分钟,20分钟,30分钟…….需要创建很多交换机和队列来路由消息。
  • 如果单独设置消息的TTL,则可能会造成队列中的消息阻塞——前一条消息没有出队(没有被消费),后面的消息无法投递(比如第一条消息过期TTL是30min,第二条消息TTL是10min。10分钟后,即使第二条消息应该投递了,但是由于第一条消息还未出队,所以无法投递)。
  • 可能存在一定的时间误差。

在RabbitMQ 3.5.7︰及以后的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延时队列功能(Linux和 Windows都可用)。同时插件依赖Erlang/OPT 18.0及以上。

插件源码地址

插件下载地址

2.3.3.1、插件安装

1、进入插件目录

whereis rabbitmq
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.12/plugins

2、将下载的插件rabbitmq_delayed_message_exchange-3.8.0.ez上传到这个

3、启用插件

# 启动插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

# 停用插件
rabbitmq-plugins disable rabbitmq_delayed_message_exchange

4、使用插件

通过声明一个x-delayed-message类型的Exchange来使用delayed-messaging特性。x-delayed-message是插件提供的类型,并不是rabbitmq本身的(区别于direct、topic、fanout、headers)。

image-20210714182739981

2.3.3.2、使用代码示例

示例:声明延迟Exchange

@Bean("delayExchange")
public TopicExchange exchange() {
    Map<String, Object> argss = new HashMap<String, Object>();
    argss.put("x-delayed-type", "direct");
    return new TopicExchange("DELAY_EXCHANGE", true, false, argss);
}

生产者:消息属性中指定x-delay参数。

示例

MessageProperties messageProperties = new MessageProperties();
// 延迟的间隔时间,目标时刻减去当前时刻
messageProperties.setHeader("x-delay", delayTime.getTime() - now.getTime());
Message message = new Message(msg.getBytes(), messageProperties);

// 不能在本地测试,必须发送消息到安装了插件的服务端
rabbitTemplate.send("DELAY_EXCHANGE", "#", message);

补充:消息除了过期,还有什么情况下会变成死信消息?

  1. 消息被消费者拒绝并且未设置重回队列:(NACK || Reject ) && requeue ==false
  2. 队列达到最大长度,超过了Max length (消息数)或者Max length bytes (字节数),最先入队的消息会被发送到DLX。

2.4、消息存储限制

场景:当RabbitMQ生产MQ消息的速度远大于消费消息的速度时,会产生大量的消息堆积,占用系统资源,导致机器的性能下降。我们想要控制服务端接收的消息的数量,应该怎么做呢?

流量控制我们可以从几方面来控制,一个是服务端,一个是消费端。

2.4.1、服务端控制
2.4.1.1、队列长度

队列有两个控制长度的属性:

  • x-max-length:队列中存储消息的最大数量,超过这个数量,队头的消息会被丢弃。
  • x-max-length-bytes:队列中存储的最大消息容量(单位bytes),超过这个容量,队头的消息会被丢弃

image-20210714215301806

需要注意的是,设置队列长度只在消息堆积的情况下有意义,而且会删除先入队的消息,不能真正地实现服务端限流。

2.4.1.2、内存控制

RabbitMQ会在启动时检测机器的物理内存数值。默认当MQ占用40%以上内存时,MQ会主动抛出一个内存警告并阻塞所有连接(Connections)。可以通过修改rabbitmq.config文件来调整内存阈值,默认值是0.4,如下所示:
Windows默认配置文件: advanced.config,在此路径下 C:\Users\用户名\AppData\RoamingRabbitMQ\

[{rabbit,[{vm_memory_high_watermark,0.4}]}].

也可以用命令动态设置,如果设置成0,则所有的消息都不能发布。

rabbitmqctl set_vm_memory_high_watermark 0.3

image-20210714220621038

2.4.1.3、磁盘控制

相关配置参数,参考文档 https://www.rabbitmq.com/configure.html#config-items

另一种方式是通过磁盘来控制消息的发布。当磁盘剩余可用空间低于指定的值时(默认50MB),触发流控措施。

# 指定磁盘的30%,存储消息
disk_free_limit.relative = 3.0
# 指定磁盘的2GB,用来存储消息
disk_free_limit.absolute = 2GB

还有一种情况,虽然 Broker消息存储得过来,但是在push模型下(consume,有消息就消费),消费者消费不过来了,这个时候也要对流量进行控制。

2.4.2、消费端控制

https://www.rabbitmq.com/consumer-prefetch.html

默认情况下,如果不进行配置,RabbitMQ会尽可能快速地把队列中的消息发送到消费者。因为消费者会在本地缓存消息,如果消息数量过多,可能会导致OOM或者影响其他进程的正常运行。

在消费者处理消息的能力有限,例如消费者数量太少,或者单条消息的处理时间过长的情况下,如果我们希望在一定数量的消息消费完之前,不再推送消息过来,就要用到消费端的流量限制措施。

可以基于Consumer或者channel设置prefetch count的值,含义为Consumer端的最大的 unacked messages数目。当超过这个数值的消息未被确认,RabbitMQ会停止投递新的消息给该消费者。

channel.basicQos(2); //如果超过2条消息没有发送ACK,当前消费者不再接受队列消息
channel.basicConsume(QUEUE_NAME, false, consumer); // 这里设置为手动提交消息

代码示例

启动两个消费者,其中一个Consumer2消费很慢,qos设置为2,最多一次给它发两条消息,其他的消息都被Consumer1接收了。这个叫能者多劳。

3、RabbitMQ使用

3.1、Springboot整合RabbitMQ

参考代码:https://gitee.com/fanger8848/study/tree/master/rabbitMQ/springboot-demo

maven依赖

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

RabbitConfig.java

@Configuration
public class RabbitConfig {

    // 两个交换机
    @Bean("topicExchange")
    public TopicExchange getTopicExchange(){
        return new TopicExchange("TOPIC_EXCHANGE");
    }

    @Bean("fanoutExchange")
    public FanoutExchange getFanoutExchange(){
        return  new FanoutExchange("FANOUT_EXCHANGE");
    }

    // 三个队列
    @Bean("firstQueue")
    public Queue getFirstQueue(){
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-message-ttl",6000);
        Queue queue = new Queue("FIRST_QUEUE", false, false, true, args);
        return queue;
    }

    @Bean("secondQueue")
    public Queue getSecondQueue(){
        return new Queue("SECOND_QUEUE");
    }

    @Bean("thirdQueue")
    public Queue getThirdQueue(){
        return new Queue("THIRD_QUEUE");
    }

    // 两个绑定
    @Bean
    public Binding bindSecond(@Qualifier("secondQueue") Queue queue,@Qualifier("topicExchange") TopicExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("#.gupao.#");
    }

    @Bean
    public Binding bindThird(@Qualifier("thirdQueue") Queue queue,@Qualifier("fanoutExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

}

消费者

@Component
@RabbitListener(queues = "FIRST_QUEUE")
public class FirstConsumer {

    @RabbitHandler
    public void process(String msg){
        System.out.println(" first queue received msg : " + msg);
    }
}

消息发送

@Component
public class MyProvider {

    @Autowired
    AmqpTemplate amqpTemplate;

    public void send(){
        // 发送4条消息

        amqpTemplate.convertAndSend("","FIRST_QUEUE","-------- a direct msg");

        amqpTemplate.convertAndSend("TOPIC_EXCHANGE","shanghai.teacher","-------- a topic msg : shanghai.teacher");
        amqpTemplate.convertAndSend("TOPIC_EXCHANGE","changsha.student","-------- a topic msg : changsha.student");

        amqpTemplate.convertAndSend("FANOUT_EXCHANGE","","-------- a fanout msg");

    }

}

3.2、Springboot参数配置

Springboot相关配置查询地址 https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html

4、特性总结

  • 支持多客户端:对主流开发语言(Python、Java、Ruby、PHP、C#、JavaScript、Go、Elixir、Objective-C、 Swift等)都有客户端实现。
  • 灵活的路由:通过交换机 (Exchange)实现消息的灵活路由。
  • 权限管理:通过用户与虚拟机实现权限管理。
  • 插件系统:支持各种丰富的插件扩展,同时也支持自定义插件。
  • 与Spring集成: Spring对 AMQP进行了封装。
  • 高可靠: RabbitMQ提供了多种多样的特性让你在可靠性和性能之间做出权衡,包括持久化、发送应答、发布确认以及高可用性。
  • 集群与扩展性:多个节点组成一个逻辑的服务器,支持负载。
  • 高可用队列:通过镜像队列实现队列中数据的复制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值