消息队列中间件之RabbitMQ(下)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

上篇博客有简单介绍过MQ的概念以及基本使用,本篇博客将进阶MQ稍微高级一些的知识以及面试可能会问到的一些问题,总体围绕消息来展开。
消息由生产者生产,再放至队列中,再分发给消费者消费。这是最普通也是最正常的情景,但就这普普通通的几句话中,在实际应用中可能会出现各种各样的问题,考虑最坏的情况,来模拟消息的九九八十一难,比如:
生产者生产消息后不能成功发至队列中,发至队列中后,队列又丢失了消息,消息到了队列中,却发不到消费者那,消费者接收了,确没有真正消费,这要怎么处理?
有些业务逻辑要求该消息存活一定的时间,比如短信验证码,十分钟内不使用验证码就失效,这个时候消息要怎么设置,设置完成了却在以上说的环节出了问题或者消息没消费过时了又或者消息消费后过时了,这种消息又要怎么处理?
也有些业务逻辑要求该消息在一定时间后才能生效,比如用户注册一个星期后才有大优惠卷送,这个时候总不能写个脚本定时处理吧?
还有些情况要跟踪消息的去处、消息的消费顺序、消息去除重复消费等情况的, 下文将做细究以及延伸探讨。


提示:以下是本篇文章正文内容,下面案例可供参考

RabbitMQ

在实际开发中,大多数程序员都是使用SpringBoot整合RabbitMQ进行开发,后面要讲解的内容也是在此基础上进行的,为此先整合好RabbitMQ。

一、 SpringBoot整合RabbitMQ

服务端启动RabbitMQ

[root@localhost ~]# systemctl start rabbitmq-server 
[root@localhost ~]# systemctl status rabbitmq-server
● rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled; vendor preset: disabled)
   Active: active (running) since 五 2021-01-15 21:13:37 CST; 19min ago
 Main PID: 7856 (beam.smp)
   Status: "Initialized"
    Tasks: 85
   CGroup: /system.slice/rabbitmq-server.service
           ├─7856 /usr/lib64/erlang/erts-10.4.4/bin/beam.smp -W w -A 64 -MBas ageffcbf -MHas ageffcbf -MBlmbcs 512 -MHlmbcs 512 -MMmcs 30 -P 1048576 -t 5000000 -stbt db -zdbbl 128000 -K true -- -root /usr/lib64/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /...
           ├─8074 /usr/lib64/erlang/erts-10.4.4/bin/epmd -daemon
           ├─8212 erl_child_setup 32768
           ├─8233 inet_gethost 4
           └─8234 inet_gethost 4

1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: ##  ##
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: ##  ##      RabbitMQ 3.7.18. Copyright (C) 2007-2019 Pivotal Software, Inc.
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: ##########  Licensed under the MPL.  See https://www.rabbitmq.com/
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: ######  ##
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: ##########  Logs: /var/log/rabbitmq/rabbit@localhost.log
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: /var/log/rabbitmq/rabbit@localhost_upgrade.log
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: Starting broker...
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: systemd unit for activation check: "rabbitmq-server.service"
1月 15 21:13:37 localhost.localdomain systemd[1]: Started RabbitMQ broker.
1月 15 21:13:37 localhost.localdomain rabbitmq-server[7856]: completed with 3 plugins.
[root@localhost ~]# systemctl status firewalld.service 
● firewalld.service - firewalld - dynamic firewall daemon
   Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)
   Active: inactive (dead)  
     Docs: man:firewalld(1)
     

启动rabbitMQ
systemctl start rabbitmq-server

查看RabbitMQ服务状态
systemctl status rabbitmq-server

查看防火墙状态
systemctl status firewalld.service
Active: inactive (dead) 关闭状态 active (running) 开启状态

关闭防火墙有临时关闭也有永久关闭,在我们学习过程中,建议永久关闭,不然每次一开虚拟机,外面需要连接虚拟机内部服务的时候,就得关闭一次防火墙,显得很麻烦。
systemctl stop firewalld #临时关闭
systemctl disable firewalld #永久关闭,即设置开机的时候不自动启动

登录RabbitMQ web 管理页面
http://192.168.23.129:15672/

配置配置文件

spring:
  application:
    name: springboot_rabbitmq
  rabbitmq:
    host: 192.168.23.129
    port: 5672
    username: ems
    password: 123
    virtual-host: /ems

RabbitTemplate 用来简化操作 使用时候直接在项目中注入即可使用

hello world模型

开发生产者

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void testHello(){
  rabbitTemplate.convertAndSend("hello","hello world");
}

开发消费者

@Component
@RabbitListener(queuesToDeclare = @Queue("hello"))
public class HelloCustomer {

    @RabbitHandler
    public void receive1(String message){
        System.out.println("message = " + message);
    }
}
work模型

开发生产者

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void testWork(){
  for (int i = 0; i < 10; i++) {
    rabbitTemplate.convertAndSend("work","hello work!");
  }
}

开发消费者

@Component
public class WorkCustomer {
    @RabbitListener(queuesToDeclare = @Queue("work"))
    public void receive1(String message){
        System.out.println("work message1 = " + message);
    }

    @RabbitListener(queuesToDeclare = @Queue("work"))
    public void receive2(String message){
        System.out.println("work message2 = " + message);
    }
}

说明:默认在Spring AMQP实现中Work这种方式就是公平调度,如果需要实现能者多劳需要额外配置

Fanout 广播模型

开发生产者

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void testFanout() throws InterruptedException {
  rabbitTemplate.convertAndSend("logs","","这是日志广播");
}

开发消费者

@Component
public class FanoutCustomer {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue,
            exchange = @Exchange(name="logs",type = "fanout")
    ))
    public void receive1(String message){
        System.out.println("message1 = " + message);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue, //创建临时队列
            exchange = @Exchange(name="logs",type = "fanout")  //绑定交换机类型
    ))
    public void receive2(String message){
        System.out.println("message2 = " + message);
    }
}
Route 路由模型

开发生产者

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void testDirect(){
  rabbitTemplate.convertAndSend("directs","error","error 的日志信息");
}

开发消费者

@Component
public class DirectCustomer {

    @RabbitListener(bindings ={
            @QueueBinding(
                    value = @Queue(),
                    key={"info","error"},
                    exchange = @Exchange(type = "direct",name="directs")
            )})
    public void receive1(String message){
        System.out.println("message1 = " + message);
    }

    @RabbitListener(bindings ={
            @QueueBinding(
                    value = @Queue(),
                    key={"error"},
                    exchange = @Exchange(type = "direct",name="directs")
            )})
    public void receive2(String message){
        System.out.println("message2 = " + message);
    }
}

Topic 订阅模型(动态路由模型)

开发生产者

@Autowired
private RabbitTemplate rabbitTemplate;

//topic
@Test
public void testTopic(){
  rabbitTemplate.convertAndSend("topics","user.save.findAll","user.save.findAll 的消息");
}

开发消费者

@Component
public class TopCustomer {
    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,
                    key = {"user.*"},
                    exchange = @Exchange(type = "topic",name = "topics")
            )
    })
    public void receive1(String message){
        System.out.println("message1 = " + message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,
                    key = {"user.#"},
                    exchange = @Exchange(type = "topic",name = "topics")
            )
    })
    public void receive2(String message){
        System.out.println("message2 = " + message);
    }
}


二、 RabbitMQ进阶

1、TTL

TTL是time to live的简称,描述消息的存活时间。在日常生活中,随处可见TTL。比如登录是的验证码,一般有效期就是5分钟,也就是说这个消息(验证码),5分钟后失效。

这个其实比较简单。设置过期时间,通常来说有俩种,一是设置队列的过期时间,二是设置消息的过期时间。后者更细更精确,若同时设置,则哪个值小,就以哪个为准。
在这里插入图片描述

管理界面分别会有EXP和TTL的标识在这里插入图片描述

x-expires对应整个队列,x-message-ttl 对应单个消息。如果都不设置,默认不过期,永久有效。那么一旦过期了,则会来到死信队列(DLX)。

2、DLX

Dead-Letter-Exchange,意为死信队列。它其实也是一个正常的交换器,它可以作为队列的属性设置在任何队列上。比如A队列,设置死信队列的属性,那么A队列中存在死信就会发送到刚才设置的死信队列中。

代码也是很简单,和TTL一样也是通过channel.queueDeclare最后一个参数中设置的属性

    args . put("x-dead-letter-exchange" , " dlx_exchange ");

在ok1队列中声明了一个名为dlx_exchange的死信队列,ok1队列中一旦产生了死信,就会发送到dlx_exchange死信队列中,如果没有路由指定,则使用原来队列(ok1)的路由键,如果要指定死信队列的路由,则用

   args.put("x-dead-letter-routing-key" , "dlx-routing-key");

即可指定特殊的路由键,关键字再重复一下

x-dead-letter-exchange 指定死信队列的名称

x-dead-letter-routing-key 指定死信队列的路由键

这俩个属性 分别在管理界面对应DLX、DLK的标志,在实际应用中DLX是很有用的一个特性,设置不同的路由键可以知道死信消息的异常情况。

消息变成死信 一般是由于以下几种情况:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度
3、延迟队列

延迟队列就是延迟生效的队列,运用TTL和DLX就可以做到,生产一个消息,设置TTL为5秒,5秒后到DLX队列中,那么再从DLX消费。这就完成了延迟队列的功能。这种应用场景,再生活中也常见,比如买票,抢到票后没有支付,过了超过三十分钟来到了死信队列,这个时候,就要将抢到的票再退回到系统中,就是要从死信队列中去消费。

4、优先级队列

通过队列的权重,来决定队列的优先级。最大为10最小为0。前提是有消息堆积,如果没有消息堆积,相当于容器中同一时刻只有一条消息,单条消息优先级没有意义。

args.put( "x-rnax-priority" , 10) ;
5、消息传输保障

消息传输保障,这是很重要的。如果在生产到消费环节,存在消息丢失。那么很可能会导致严重的生产事故。在生产环境中,也是很容易出现各种各样的情况,下面尽可能的写出所遇到情况和应对措施。

5.1、生产者丢失消息

我们都知道,简单的消费模型就是生产者生产消息到队列中,然后消费者从队列中拿消息去消费。那么这些简单的环节是很有可能出错的,先说生产者。生产者生产消息发送到队列中去,该途中断电断网或者服务器崩溃啥的,导致消息发出去了但是队列却又没收到。对此,RabbitMQ提供了俩种解决方式。一是事务机制,二是confirm模式

5.1.1、事务机制

相信事务应该都很熟悉了,其相关概念这里就不提了。RabbitMQ与事务相关的三个方法

  • 开启事务 channel . txSelect()
  • 提交事务 channel . txCommit()
  • 回滚事务 channel txRollback()

代码简略
在这里插入图片描述
当消息毫无异常,正确的发送了。那么事务就提交,如果存在异常,事务回滚。但是这样写,消耗很大的性能,严重影响吞吐量,所以有了confirm模式,平常都是用的confirm模式来确认消息是否从生产者发送到队列中。

5.1.2、confirm机制

confirm模式全称就是发送方确认机制,生产者将信道设置成 confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从1 开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 会发送一个确认(Basic.Ack) 给生产者(包含消息的唯一 ID) ,这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。
在这里插入图片描述

发送方确认机制最大的好处就是它是异步的,而事务机制是同步的。另外二者机制是互相排斥的,不能同时用俩个机制,否则会报错。

5.2、队列丢失消息

当消息成功发送到队列中,消息正排着对等着消费,此时异常情况(宕机,停电等)发生了。那么没有消费的消息必定丢失,为此RabbitMQ提供了队列持久化的机制。当消息一来到队列,就立马写入磁盘中。这里有几个概念需要主要,消息持久化,队列也得持久化,否则消息的宿主都没有持久化,消息的持久化显的毫无意义。当然交换器也得持久化。在队列这个环节,要想保障消息的安全。得把交换器、队列、消息都做持久化设置。但是这样做必定消耗一定的性能,所以再性能以及安全方面得做好权衡。下面简单介绍设置他们的持久化。

 //设置交换机 持久化 参数3 是否持久化  true 则设置交换机为持久化
            channel.exchangeDeclare("exchange","fanout",true);
            //设置队列持久化   参数2: 是否持久化  true 则设置队列为持久化
            channel.queueDeclare("ok1",true,false,false,args);//声明队列
            //设置消息持久化  参数3 MessageProperties.PERSISTENT_TEXT_PLAIN 该属性表示消息持久化
            channel.basicPublish("ok1","", MessageProperties.PERSISTENT_TEXT_PLAIN,"hello rabbitmq111".getBytes());//发布队列

5.3、消费者丢失消息

通过前俩小节的保障,消息就等着被消费了,当然这个环节也是有可能会出现异常情况的。比如一个消费者从队列中拿到一个消息,进行一系列逻辑处理,但是没开始处理或者说没处理完成就宕机了,那么这个消息就出现了异常丢失。在生产环境中,拿到一个消息后通常是有多个逻辑处理的,而且这些逻辑是有事务性的,比如一个消息需要经过ABCD四个方法处理,其中任何一个方法报错就会回滚,要么四个方法都成功,要么都不成功。所以一般不会存在消息被处理了一半的情况,另外值得提醒的是,如果消息回滚,那么这不是消息的丢失,这是正常的情况。需要你去对异常数据进行处理。

言归正传,为了防止消息在消费者这里丢失,RabbitMQ也有应对机制,该机制和发布者确认机制概念类似,称作消息确认机制。可以看官网的消息确认机制,我上一篇博客也有3.4第5点也有写到过。这里就不多写了。值得补充的是,一旦消息者开启手动确认机制,如果消息迟迟没有得到回复或者是回复false又或是拒绝接受,那么都称为未被确认的消息,则会重新加入队列中,等待下次消费。

6、消息应用保障

上一大节,基本上解决了消息的传输问题,那么消息的应用也有很多情况需要我们考虑。下面尽可能的多介绍几种问题以及解决方案。

6.1、消息堆积

在生产环境中,消息堆积是很容易出现的,当消息的生产速度长时间大于消费速度时,就会造成消息堆积,出现这种情况有可能是生产者突然大量发布消息,被黑客恶意攻击。也有可能消费者消费的时候出现了问题,也有可能消费者性能到达了一定的瓶颈,需要升级硬件或优化代码。还有其他一些奇葩原因,不管是啥原因引起的消息堆积,这种影响在生产环境中影响是很大的。

堆积到一定程度,新消息无法进入队列。而且要是有TTL的消息,就会更加麻烦。本该在过期时间内处理,却因为消息堆积没有处理又失效了。消息迟迟不处理,或等待时间超过了规定范围。总之影响是很大的。

解决思路:首先看日志就不用说了吧,确定消费是否存在异常,导致消息处理无效。还是消息处理很慢,根据业务场景以及堆积的原因先把代码的可行性确定好,代码一旦通了,需要考虑消息堆积的量,如果量很大,还是有必要部署多个消费者,相当于搞集群。还可以搞个线程池,开多个线程去消费。

6.2、消息乱序

消息顺序,我个人认为是很考验设计能力的一个方面。先举个最简单的情景,生产者先后生产A、B、C三个消息,要求消费者也按A、B、C三个顺序依次消费,但是如果是消费者是队列模式,就会多个消费者去抢ABC消费,其消费顺序得不到保障,我看一些博主甚至讲师,对待这一情况的解决方案,是根据不同消息生成不同的队列,每一个队列只有一个消费者,那么A一发送就会被消费,依次类推。消息顺序就是ABC。这是理想环境,但是实际上情况复杂的多,比如A异常了,被消费者拒绝而从新返回队列了,而此时BC正常,那么消费顺序就是BCA了,另外要是A消费的慢,或者卡住了,此时也是会乱序。另外还有队列优先级、TTL过期等情况。
个人认为,对于消息的顺序有严格要求的话。尽量将逻辑按顺序写在一起,比如淘宝购物的场景,用户退货。消息1确认收到货,消息二给用户退钱,消息三通知用户退货成功。按123的顺序来执行,是严格要求的。要是钱还没退,就通知用户退货成功了,用户岂不是会多次问候? 那么这种情况,个人不建议像上一段一样,把一个消息对应一个队列,一个队列对应一个消费者。我建议把消息统一,把逻辑排序。就一个消息,但是先执行确认收货再执行退钱再执行通知的逻辑,且给逻辑提供事务性。
总之,如果要保证消息的顺序性,需要根据业务对MQ进一步的处理。

6.3、重复消费

在5.3、消费者丢失消息这一节,介绍了消费者确认机制。当一个消息被消费了但是由于异常原因还没有确认,那么可能会回到队列再次消费。对于这种情况,建议把消费处理逻辑和消息确认机制都事务化,处理了就确认,没处理完就不确认,就不会存在处理但没确认的数据,也就不会出现同一消息二次处理的情况。
当然也有可能是俩条一模一样的消息要消费者去消费,这也是不正常的,但有可能发生的。对此我们可以给消息一个全局的唯一id。比如hash值,一样的消息,hash一样。每次消费后存入一个表,每次消费的时候判断这个hash是否存在,如果存在那么就是消费过的。对于这个方案,建议使用redis的setnx命令。单线程,高并发情况下也不会出现同时存入俩个一样的id,安全可靠。

7、RabbitMQ集群
7.1 普通集群

普通集群,也叫做主从复制集群。其消息队列位于主节点上,但是从节点可以看到和访问,具体操作还是有所限制。
在这里插入图片描述
当主节点宕机,从节点可以对队列进行备份。这种模式,不会自动故障转移,单单只是备份。

7.2 镜像集群

镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。

在这里插入图片描述
还会用到HAproxy做负载均衡,keepAlived保证HAProxy的高可用性,实现自动故障转移。

总结

综上 RabbitMQ大部分都介绍完了,后面还会写关于其他MQ的博客。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值