RabbitMQ入门

1、MQ相关概念

1、概念及作用

MQ消息队列,遵循先入先出原则,是一种跨进程的通信机制,用于上下游传递消息。

作用:

1、流量削峰

当某个系统最多一秒内处理100次请求,当某次请求数打到200次,可以用MQ队列做缓冲,把一秒内200次的请求分段执行。

2、应用解耦

3、异步处理

2、MQ四大核心概念

1、生产者

产生数据发送消息的程序是生产者

2、交换机

交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中,交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者把消息丢弃,这个由交换机的类型决定

3、队列

队列是RabbitMQ内部使用的一种数据结构,尽管消息经RabbitMQ和应用程序,但是消息只能存储在队列中。队列仅受主机的额内存和磁盘限制的约束,本质上是一个大的消息缓冲区,生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。

4、消费者

消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。生产者,消费者和消息中间件很多时候并不在同一个机器上。同一个应用程序既可以是生产者又可以是消费者。

3、名词介绍

1、Broker

接收和分发消息的应用,RabbitMQ Server就是message broker

2、Virtual host

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

3、Connection

publisher / consumer 和 broker 之间的TCP连接

4、Channel

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

5、Exchange

message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型:direct,topic and fanout

6、Queue

消息最终被送到这里等待consumer取走消费

7、Binding

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

4、消息生产者与消费者

示例代码one

5、Work Queues

工作队列,又称为任务队列,主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业,当有多个工作线程时,这些工作线程将一起处理这些任务。

5.1、轮训分发消息

消息生产者发送的消息,被两个消费线程轮流获取,按照A一个消息,B一个消息的模式进行消费

示例代码two

5.2、消息应答

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然就宕机了。RabbitMQ一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然消费者宕机,将丢失正在处理的消息。

针对这种情况,rabbitMQ引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息后,告诉rabbitmq他已经处理了,rabbitmq可以删除该消息。

5.2.1 自动应答

消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channel关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对穿的消息数量进行限制,当然这样有可能是的消费者这边由于接收到太多还来不及处理的消息,导致消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。

5.2.2 消息应答的方法

Channel.basicAck — 用于肯定确认,RabbitMQ已经知道该消息并成功的处理消息,可以将其丢弃了

Channel.basicNack — 用于否定确认

Channel.basicReject — 用于否定确认 与basicNack相比少一个参数,不处理该消息了直接拒绝,可以将其丢弃了

5.2.3 Multiple的解释

手动应答的好处是可以批量应答并且减少网络拥堵

/**
* multiple:是否批量应答,true代表批量应答 false非批量应答
*/
channle.basicAck(deliveryTag,multiple);

示例代码three

5.2.4消息自动重新入队

如果消费者由于某些原因失去连接,导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,他将很快将其重新分发给另一个消费者。以确保消息不会丢失。

示例代码three

6、RabbitMQ持久化

6.1、队列实现持久化

队列若没进行持久化设置,rabbitmq如果重新,该队列就会被删除,丢失消息,如果需要队列实现持久化,需要在声明队列的时候把durable参数设置为持久化

boolean durable = true;//设置消息队列为持久化
channel.queueDeclare(ACK_QUEUE_NAME,durable,false,false,null);

需要注意的是,同一个队列,若之前该队列不是持久化的,需要将该队列删除后,在进行设置,不然就会报错。持久化的队列,在rabbitMQ服务重新启动后,队列也不会丢失

6.2、消息实现持久化

消息持久化需要在消息生产者处修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN添加这个属性

channel.basicPublish("",TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes("utf-8"));

将消息标记为持久化并不能完全保证不会丢失消息,尽管告诉RabbitMQ将消息保存到磁盘,但这里依然存在当消息刚准备存储时,但还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘,持久性保证并不强,如果需要更强有力的持久化策略,可以使用发布确认。

示例代码three

6.3、不公平分发

RabbitMQ分发消息默认采用轮询分发模式,但是在某些模式下并不适用,比如,两个消费之AB,A处理任务很快,但B很慢,轮询分发会让A一直出去空闲等待,而B一直在干活,影响效率。

为了避免这种情况,可以设置channel.basicQos(1);

这样就可以将轮询模式改为不公平分发模式,同样的消息消费者AB,A可以消费3个消息,二B只用消费1个消息,有可能A3个结束B的1个还在继续,当后面还又消息,RabbitMQ会把该消息优先分发给A去进行消费,这样就可以提高效率,避免A线程有过多的空闲时间。

6.4、预取值

本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认。

示例代码three

7、发布确认

7.1、发布确认原理

生产者将信道设置为confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列后,broker就会发送一个确认给生产者(包含唯一id),这就是的生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,标识这个序列号之前的所有消息都已经得到了处理。

confirm模式最大的好在于他是异步的,一旦发布一条消息,生产者应用程序就可以等信道返回确认的同时继续发送下一条消息,当消息最终得到确认后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。

7.2、开启发布确认的方法

发布确认默认是没有开启的,如果要开启需要调用方法confirmSelect,每当你要想使用发布确认,都需要在channel上调用该方法

Channel channel = connection.createChannel();
channel.confirmSelect();

7.3、单个确认发布

这是一种简单的确认方式,他是一种同步确认发布的方式,也就是发布一个消息之后只有他被确认发布,后续的消息才能继续发布,waitForConfrimsOrDie(long)这个方法只有在消息被确认的时候返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

这种确认方式有一个最大的缺点就是:发布速度特别慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。

7.4、批量确认发布

单个确认发布方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大的提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而重新发布消息,这种方案仍然是同步的,也一样会阻塞消息发布。

7.5、异步确认发布

异步确认性价比最高,无论是可靠性还是效率都很高,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。

7.6、如何处理异步未确认消息

最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如用ConcurrentLinkedQueue这个队列在confirm callbacks与发布线程之间进行消息的传递。

示例代码four

8、交换机

在未使用交换机的时候,消息生产者所生产的消息,只能被一个消费者进行消费,不能被其他消费者消费,交换机可以将生产的消息,按照一定的规则,分发给不同的消费者去进行消费,实现一个消息可以被多个消费者消费,这种模式称为“发布/订阅”

8.1、Exchanges

RabbitMQ消息传递模型的核心思想是:生产者产生的消息从不会直接发送给队列, 实际上,通常生产者甚至都不知道这些消息传递到那些队列中。

相反,生产者只能将消息发送到交换机(exchange),交换机一方面它接收来自生产者的消息,另一方面将他们推入队列。交换机必须确切知道如何处理收到的消息,比如是该把消息放到特定的队列还是将消息放到多个队列,或者丢弃他们,这就由交换机的类型来决定了。

消息生产者 —> 交换机exchange —> 队列queue

8.2、Exchanges类型

1、直接(direct)

2、主题(topic)

3、标题(headers)

4、扇出(fanout)

8.3、默认exchange

当我们在发布消息时,不指定交换机,则使用的是默认交换机

channel.basicPublish("","hello",null,message.getBytes());

第一个参数是交换机的名称,空字符串表示使用默认交换机:消息能路由发送到队列,其实是由routingKey绑定key指定的,如果它存在的话

8.4、临时队列

当我们连接到rabbit时,都需要一个全新的空队列,为此我们可以创建一个具有随即名称的队列,或者能让服务器为我们选择一个随机队列名称就好了,其次一旦我们断开了消费者的连接,队列将被自动删除

创建临时队列的方法:

String queueName = channel.queueDeclare().getQueue();

8.5、绑定(binding)

binding其实就是exchange和queue之间的桥梁,它告诉我们exchange和哪个队列进行了绑定关系。

绑定是交换机和队列之间的桥梁,队列只对它绑定的交换机的消息感兴趣,绑定用参数:routingKey来表示,也可以称该参数为binding keye,创建绑定我们用代码:

channel.queueBind(queueName,exchangeName,"routingKey");

绑定之后的意义由其交换类型决定

8.6、Fanout exchange

fanout是将接收到的所有消息广播到他知道的所有队列中。

示例代码five

8.6、Direct exchange

direct类型的工作方式是,消息只去到它绑定的routingkey队列中去。

8.6.1、多重绑定

当如果exchange绑定类型是direct,但是他绑定的多个队列的key如果都相同,在这种情况下对然绑定类型是direct,但是他表现的就和fanout有点类似。

channel.queueBind(queueName,EXCHANGE_NAME,"routingKey1");
channel.queueBind(queueName,EXCHANGE_NAME,"routingKey2");

示例代码six

8.7、Topics exchange

主题模式可以根据特定的routingkey规则进行匹配,routingkey必须满足一个要求,他必须是一个单词列表,以点号分开。这些单词可以是任意单词,如: “a.abc.c”,"new.old"这种类型的,当然这个单词列表不能超过255个字节

这个规则中,有两个替换符:
*:可以代替一个单词
#:可以替代零个或多个单词
当一个队列绑定键是#,那么这个队列接收所有数据,类似fanout了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct了

示例代码seven

9、死信队列

9.1、死信概念

死信指无法被消费的消息,一般消息生产者将消息发送给broker或者直接发给queue,消费者从queue中取出消息进行消费,但某些是由特定的原因导致队列中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列

应用场景:为了保证订单业务的消息数据不丢失,需要使用到rabbitmq的死信队列机制,当消息消费发生异常时,将消息投入到死信队列中。

或者用户在商城下单成功并点击支付后,在指定时间未支付自动失效。

9.2、死信来源

消息TTL过期

队列打到最大长度(队列满了,无法再添加数据到mq中)

消息被拒绝(basic.reject 或 basic.nack)并 且 requeue = fase

示例代码eight

10、延迟队列

队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,延时队列就是用来存放需要在指定时间被处理的元素队列

10.1、延时队列使用场景

1、订单在十分钟之内未支付则自动取消

2、新创建的店铺,如果在10天内都没有上传过商品,则自动发送消息提醒

3、用户注册成功后,如果三天内没有登陆则进行短信提醒

4、用户发起退款,如果三天内没有得到处理,则通知相关运营人员

5、预定会议后,需要在预定的时间点前10分钟通知各个参会人员参与会议

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭,看起来似乎使用了定时任务,一直轮询数据,每秒查询一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭”,短期内未支付的订单数据可能会很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

11、RabbitMQ中的TTL

TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者队列中所有消息的最大存活时间。

TTL单位是毫秒

如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值会被使用,设置TTL有两种方式。

11.1 、消息设置TTL

在发送消息时,给每一条消息设置 对应的TTL

rabbitTemplate.convertAndSend("X","XC",msg,(message)->{
            //设置发送消息的时候延迟时长 单位ms
            message.getMessageProperties().setExpiration(ttlTime);
            return message;
        });

11.2、队列设置TTL

声明队列时,给队列"x-message-ttl"属性设置TTL

public Queue queueA(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信routingkey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置TTL
        arguments.put("x-message-ttl",10000);

        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

11.3、两者区别

如果设置了队列TTL属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列,会被丢给死信队列中)。

如果给消息设置了TTL,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长的时间,另外,还需要注意的一点是,如果不设置TTL,表示消息永远不会过期,如果将TTL设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃

示例代码springboot-rabbitmq下

11.4、RabbitMQ延时队列存在的问题

RabbitMQ只会检查第一个消息是否过期,如果过期则丢弃或者进入死信队列,如果第一个消息的延迟时间很长,而第二个消息延迟时间很短,那么第二个消息并不会优先执行,必须等排在它前面的消息消费后,才会执行。

解决方案:使用RabbitMQ延时队列插件:rabbitmq-delayed-message-exchange

11.5、总结

延时队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延时队列可以很好利用RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过RabbitMQ集群的特性,可以很好的解决单点故障问题,不会因为单个节点 挂掉导致延迟队列不可用或者消息丢失

当然,延时队列还有很多其他的选择,比如:Java的DelayQueue,利用redis的zset,利用Quartz或者利用kafka的时间轮,这些方式各有特点,看需要适用的场景。

12、示例代码

https://gitee.com/cpfr/study-rabbit-mq

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值