RabbitMQ 基础学习

一、为什么要引入MQ/RabbitMQ(中间件),直接读写数据库不行吗?

1、在分布式系统下中间件具备异步处理,流量削峰等一系列高级功能;

2、中间件可以实现生产者和消费者之间的解耦。

3、拥有持久化的机制,进程消息,队列中的信息也可以保存下来。

4、对于高并发场景下,利用消息队列可以使得同步访问变为串行访问达到一定量的限流,利于数据库的操作。

5、可以使用消息队列达到异步下单的效果,后台进行逻辑下单。

二、什么是MQ?

RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件,核心思想是生产者不会将消息直接发送给队列,消息在发给客户端时会先发给交换机,然后再由交换机发送给对应的队列。

三、RabbitMQ有什么优缺点?

优点:
1、解耦

系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

2、异步

将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

3、削峰

并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

缺点:
1、一致性问题

A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产生数据不一致的问题。

2、系统的可用性降低

系统引入的外部依赖越多,系统越容易挂掉,本来只是A系统调用BCD三个系统接口就好,ABCD四个系统不报错整个系统会正常运行。引入了MQ之后,虽然ABCD系统没出错,但MQ挂了以后,整个系统也会崩溃。

3、系统的复杂性提高

引入了MQ之后,需要考虑的问题也变得多了,如何保证消息没有重复消费?如何保证消息不丢失?怎么保证消息传递的顺序?

其他MQ产品:

ActiveMQ: 老牌的MQ产品,Apqche顶级开源项目,基于JMS规范的实现,单机吞吐量在万级,高并发场景下不是最好的选择,消息可靠性差。java写的

Kafka: 大数据中使用较多,支持更高的并发量,10W+。 不支持消息的回溯。性能卓越。基于自定义TCP的二进制协议。

RocketMQ:阿里的开源产品,目前支持java及c++语言。

RabbitMQ:erlang语言实现消息中间件,支持java、phthon等语言。

四、RabbitMQ组件介绍

1、Broker:简单来说就是消息队列服务器实体。
2、Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
3、Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
4、Binding:绑定,它的作用就是把exchange和queue:按照路由规则绑定起来。
5、Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
6、vhost:虚拟主机,一个oroker!里可以开设多个vhost,用作不同用户的权限分离。
7、producer:消息生产者,就是投递消息的程序。
8、consumer:消息消费者,就是接受消息的程序。
9、channel:消息通道,在客户端的每个连接里,可建立多个channel,.每个channelf代表一个会话任务
在这里插入图片描述
在这里插入图片描述

五、交换机的几种类型?

1、直连交换机(Direct Exchange):根据消息携带的路由键将消息投递给对应队列,它是完全匹配、单播的模式。

2、扇型交换机(Fanout Exchange):这个交换机没有路由键概念,就算你绑了路由键也是无视的。这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。

3、主题交换机(Topic Exchange):发布订阅模式,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。*(星号)用来表示一个单词(必须出现的),#(井号)用来表示任意数量(零个或多个)单词

延伸:
1、消息怎么路由?
1.1、消息提供方->路由->一至多个队列

1.2、消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。

1.3、通过队列路由键,可以把队列绑定到交换器上。

1.4、消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则);

常用的交换器主要分为一下三种

1.fanout:如果交换器收到消息,将会广播到所有绑定的队列上

2.direct:如果路由键完全匹配,消息就被投递到相应的队列

3.topic:可以使来自不同源头的消息能够到达同一个队列。 使用topic交换器时,可以使用通配符

2、消息如何分发?
若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。通过路由可实现多消费的功能。

六、RabbitMQ的消息模型

1、简单模型:最简单的消息模型,一个生产者对应一个消费者。
default AMQP交换机 – direct
在这里插入图片描述

2、工作队列模型:一个生产者对应多个消费者。什么时候会用工作队列模型? 当消息的生产速度远超消息的消费速度时候,需要使用工作队列模式,多个消费者共同处理消息。
default AMQP交换机 – direct
注意:多个消费者是竞争关系,也就是同一个消息要么被C1消费,要么被C2消费。
在这里插入图片描述

3、发布订阅模式:Publish/Subscribe
交换机类型:fanout
(1)每个消费者监听自己的队列。

(2)生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息
在这里插入图片描述
4、路由模式

交换机类型:direct
路由模式下,对应使用的交换机是Direct交换机,生产者发送消息时需要指定routing key,交换机会根据routing key将消息投递到指定的队列
在这里插入图片描述
5、Topic 模式
交换机类型:topic
Topic主题模式采用的是Topic类型的交换机,因此是支持模糊匹配,消息能被投递到一个或多个队列中。生产者发送消息时指定routing key,Topic类型的交换机会根据routing key找到所有符合队列与交换机绑定时指定的binding key规则的队列,并将消息投递到那些队列中
在这里插入图片描述
6、RPC 模式

RPC即客户端远程调用服务端的方法 ,使用MQ可以实现RPC的异步调用,基于Direct交换机实现,流程如下:

1、客户端即是生产者就是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列。

2、服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果

3、服务端将RPC方法 的结果发送到RPC响应队列

4、客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。
在这里插入图片描述

七、如何解决使用RabbitMQ带来的问题

1、一致性问题

A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产生数据不一致的问题。

1.1 用的rabbitmq的延时队列,还有一些死信队列

死信的概念 :queue 中的某些消息无法被消费

死信的来源:
消息 TTL 过期 (TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间)
队列达到最大长度(队列满了,无法再添加数据到 mq 中)
消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
延迟队列概念:
延时队列就是用来存放需要在指定时间被处理的元素的队列
延迟队列使用场景 :

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

2、系统的可用性降低

系统引入的外部依赖越多,系统越容易挂掉,本来只是A系统调用BCD三个系统接口就好,ABCD四个系统不报错整个系统会正常运行。引入了MQ之后,虽然ABCD系统没出错,但MQ挂了以后,整个系统也会崩溃。

2.1 消息的持久化

2.2 镜像队列集群模式

镜像队列集群是RabbitMQ 真正的高可用模式,集群中一般会包含一个主节点master和若干个从节点slave,如果master由于某种原因失效,那么按照slave加入的时间排序,"资历最老"的slave会被提升为新的master。

镜像队列下,所有的消息只会向master发送,再由master将命令的执行结果广播给slave,所以master与slave节点的状态是相同的。比如,每次写消息到 queue 时,master会自动将消息同步到各个slave实例的queue;如果消费者与slave建立连接并进行订阅消费,其实质上也是从master上获取消息,只不过看似是从slave上消费而已,比如消费者与slave建立了TCP连接并执行Basic.Get的操作,那么也是由slave将Basic.Get请求发往master,再由master准备好数据返回给slave,最后由slave投递给消费者。

从上面可以看出,队列的元数据和消息会存在于多个实例上,也就是说每个 RabbitMQ 节点都有这个 queue 的完整镜像,任何一个机器宕机了,其它机器节点还包含了这个 queue 的完整数据,其他消费者都可以到其它节点上去消费数据。

(1)缺点:

① 性能开销大,消息需要同步到所有机器上,导致网络带宽压力和消耗很重

② 非分布式,没有扩展性,如果 queue 的数据量大到这个机器上的容量无法容纳了,此时该方案就会出现问题了

(2)如何开启镜像集群模式呢?

在RabbitMQ 的管理控制台Admin页面下,新增一个镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了

补充:RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式
1、单机模式:一般没人生产用单机模式
2、普通集群模式:
用于提高系统的吞吐量,通过添加节点来线性扩展消息队列的吞吐量。

让集群中多个节点来服务某个 queue 的读写操作,无高可用性,queue所在的节点宕机了,其他实例就无法从那个实例拉取数据;RabbitMQ 内部也会产生大量的数据传输。

3、系统的复杂性提高

引入了MQ之后,需要考虑的问题也变得多了,如何保证消息没有重复消费?如何保证消息不丢失?怎么保证消息传递的顺序?如何解决消息积压?

3.1 如何保证消息没有重复消费

为了防止消息在消费者端丢失,会采用手动回复MQ的方式来解决,同时也引出了一个问题,消费者处理消息成功,手动回复MQ时由于网络不稳定,连接断开,导致MQ没有收到消费者回复的消息,那么该条消息还会保存在MQ的消息队列,由于MQ的消息重发机制,会重新把该条消息发给和该队列绑定的消息者处理,这样就会导致消息重复消费。而有些操作是不允许重复消费的,比如下单,减库存,扣款等操作。

3.1.1 生产时消息重复

由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。生产者中如果消息未被确认,或确认失败,我们可以使用定时任务 +(redis/db)来进行消息重试。

3.1.2 消费时消息重复

两个思路:

  1. 不让消费端执行两次
  2. 让它重复消费了,但是不让其对业务数据造成影响
    (1)确保消费端只执行一次
    一般来说消息重复消费都是在短暂的一瞬间消费多次,我们可以使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识。
    (2)允许消费端执行多次,保证数据不受影响
    (2.1)数据库唯一键约束
    如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号。
    如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入
    两条。
    (2.2)数据库乐观锁思想
    如果消费端业务是更新操作,可以给业务表加一个 version 字段,每次更新把 version 作为条件,更新之后 version + 1。由于 MySQL 的 innoDB 是行锁,当其中一个请求成功更新之后,另一个请求才能进来,由于版本号 version 已经变成 2,必定更新的 SQL 语句影响行数为 0,不会影响数据库数据。

这个问题针对业务场景来答分以下几点:

1、 拿到这个消息做数据库的insert操作。然后给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

2、 拿到这个消息做Redis的set的操作,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。

3、 如果上面两种情况还不行。准备一个第三方介质,来做消费记录。以Redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入Redis。那消费者开始消费前,先去Redis中查询有没消费记录即可。

3.2 如何保证消息不丢失?

保证消息不丢失,可靠抵达,可以使用事务消息,但性能下降250倍,为此引入RabbitMQ的消息确认机制

publisher→Broker: confirmCallback确认模式
Exchange→Queue:returnCallback未投递到queue退回模式
Queue→consumer:消息持久化
consumer :ack机制

3.2.1可靠抵达-ConfirmCallback

– 解决消息的生产者,发送消息时候,消息没有到达交换机,消息丢失。如何解决? 引入生产者的消息确认机制confirmCallback

#主要是通过配置yml文件中的一个配置参数
spring:
  rabbitmq:
   ...
    publisher-confirm-type: correlated
    # ConfirmType 有三个值
    # 1NONE:禁用发布确认模式,是默认值
    # 2CORRELATED:发布消息成功到交换器后会触发回调方法
    # 3SIMPLE:与CORRELATED一样会触发回调方法;
    # 还可以实现发布消息成功后使用rabbitTemplate调用waitForConfirms方法等待broker节点返回发送结果
    
@Component
public class ConfirmProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void producerConfirm() {
        // 消息的生产者,消息发布确认回调函数
        // 参数1:消息数据,包含消息id、消息内容,调用convertAndSend()方法时候可以传入
        // 参数2:是否确认收到消息
        // 参数3:消息失败的原因
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            System.out.println("correlationData = " + correlationData);
            System.out.println("ack = " + ack); //false
            System.out.println("cause = " + cause);
            if (ack) {
                System.out.println("消息发送到交换机确认成功!");
            } else {
                System.out.println("消息发送到交换机失败,接下来根据业务做处理,比如:重发");
                // 也可以再次调用rabbitTemplate.convertAndSend
            }
        });
        rabbitTemplate.convertAndSend("exchange-confirm","confirm.msg","确认消息");
    }
}

消息只要被broker接收到就会执行confirmCallback,如果是cluster模式,需要所有broker接收到才会调用confirmCallback,表示message已经到达服务器。

3.2.2 持久化操作

RabbitMQ 宕机导致队列、队列中的消息丢失,相当于 RabbitMQ 弄丢消息—消息持久化

设置持久化将消息写出磁盘(重启后消息仍然存在),否则RabbitMQ重启后所有队列和消息都会丢失。
持久化分为三种:交换机持久化,队列持久化,消息持久化

3.2.2.1 交换机持久化
/**消费者类上的注解:
  *@Exchange注解的durable属性设置为true(默认也是true,不设置也可以)。这样,即使这个交换机没有队列,也不会被删除
  */
@RabbitListener(
bindings = @QueueBinding(
	value = @Queue(value = "direct.Queue",autoDelete = "true"),
	exchange = @Exchange(value = "direct.Exchange", type =
	ExchangeTypes.DIRECT,durable = "true"),
	key = "direct.Rout"
	)
)
3.2.2.2 队列持久化
@RabbitListener(
bindings = @QueueBinding(
	value = @Queue(value = "direct.Queue",autoDelete = "false",durable =
"true"),
	exchange = @Exchange(value = "direct.Exchange", type =
	ExchangeTypes.DIRECT,durable = "true"),
	key = "direct.Rout"
	)
)
3.2.2.3 消息持久化

消息持久化和前面两个稍微有点不同。消息持久化实际上就是基于确认机制去做的。默认情况下,只要消费者接收到这个消息,这个消息就从队列上被删除了。
但考虑这样一种场景,接口层接受到一个请求,然后推送一个消息,异步地去更新数据库。此时对于消
费者端来说,一拿到消息,消息就从队列上被删除,然后开始执行数据库更新,但此时数据库更新失败了,方法直接返回。但队列上已经没有这条消息了,这个更新操作不就没有完成了吗?这肯定是有问题的。所以RabbitMQ就有了消费者确认机制,只有消费者手动确认,消息才会被删除,否则该消息将一直存在
队列中,开启的方法很简单:
application.properties上加上:

spring.rabbitmq.listener.simple.acknowledge-mode=manual

意为改为手动确认。
对于消费者端:

@RabbitHandler
public void onMessage(String str,Channel channel,Message message) throws IOException {
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); //手动调用
System.out.println(str);
}

手动调用下确认即可,消息就会被删除。这一步可以放在业务逻辑的执行之后

3.2.3 可靠抵达-ReturnCallback

– 消息到达交换机后,但没有正确路由到队列。此时消息丢失了,对于消息的生产者来说是不知道的。导致业务受到影响。引入消息回退机制ReturnCallback

/**消费者类上的注解:
*/
@Component
public class ConfirmProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void producerConfirm() {
        // 交换机确认
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            System.out.println("correlationData = " + correlationData);
            System.out.println("ack = " + ack); //false
            System.out.println("cause = " + cause);
            if (ack) {
                System.out.println("消息发送到交换机确认成功!");
            } else {
                System.out.println("消息发送到交换机失败,接下来根据业务做处理,比如:重发");
            }
        });
        // 是否路由到队列的确认
        // true 表示消息通过交换机无法路由到队列时候,会把消息返回给生产者
        // false 消息无法路由到队列就直接丢弃
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 消息无法路由到队列时候,再这里可以获取消息内容
            System.out.println("=======消息回退:ReturnCallback=======");
            System.out.println("消息内容 = " + new String(message.getBody()));
            System.out.println("响应编码 = " + replyCode);
            System.out.println("消息无法路由的原因 = " + replyText);
            System.out.println("交换机 = " + exchange);
            System.out.println("路由key = " + routingKey);
        });
        rabbitTemplate.convertAndSend("exchange-topic-confirm","1confirm.msg1","确认消息");
    }
}

用到return退回模式,保证消息一定要投递到目标queue里,如果未能投递到目标queue里将调用returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。

3.2.4可靠抵达-Ack消息确认机制

在消费过程中,由于消费者代码异常或网络服务器异常,此时消息消费失败,会造成消息丢失。为了解决这个问题,我们开启消费者确认模式。

# 开启消费者手动确认模式 (channel.bacisAck)
spring:
  rabbitmq:
   ...
    listener:
      simple:
        retry:
          # 开启重试次数的限制,解决消费者出现异常死循环问题
          enabled: true
          # 最大重试次数
          max-attempts: 3
        # 开启消费者手动确认模式 (channel.bacisAck)
        acknowledge-mode: manual
//消费者手动确认消息消费; 参数1:消息id;参数2:是否批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

分析:消息消费失败了,希望把消息重投到队列中,再次进行消费,如果还是消费失败就拒绝消息


@Component
public class ConfirmConsumer {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "queue-confirm",durable = "true"),
            exchange = @Exchange(name = "exchange-confirm",type = ExchangeTypes.TOPIC),
            key = "confirm.#"
    ))
    public void handlerMsg(String msg, Channel channel, Message message) throws IOException {
        try {
            // 模拟:消费者消费消息出现异常,不做任何处理,会出现异常死循环。
            // 解决:配置重试次数
            int i = 1/0;
            System.out.println("消费者消费确认消息:" + msg);
            // 消费者手动确认消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            e.printStackTrace();
            // 问题:消息消费失败,yml配置了重试,重试后也失败了,消息丢失。
            // 解决消息丢失: 消费失败,手动重试一次;重试也失败,就拒绝?
            // 获取是否重投的标记
            Boolean redelivered = message.getMessageProperties().getRedelivered();
            System.out.println("消息是否重投标记 = " + redelivered);
            if (redelivered) {
                // 重新投递的消息: 直接拒绝掉。(丢失。?)
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
                System.out.println("消息消费异常,消息作废...");
            } else {
                // 消息没有被重投过,进行重投
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
                System.out.println("消息消费异常,消息进行重投");
            }
        }
    }
}

消费者获取到消息,成功处理,可以回复Ack给Broker。

延伸----死信队列

消息消费者消费消息出现异常, 为了避免异常的死循环,设置了重试次数。 消费消息时候开启了消费者确认模式,如果消费失败会重新投递,重新投递失败后,就拒绝了消息。此时消息丢失了。如何解决?-- 死信队列

死信队列:Dead Letter Queue , DLQ 存储的就是无法被消费的消息。

哪些消息会投递到死信队列中?

1、被拒绝的消息: channel.basicReject() 并且 requeue=false

2、消息过期 TTL

3、消息达到最大长度

@Component
public class ConfirmConsumer {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "queue-confirm",arguments = {
                    @Argument(name = "x-dead-letter-exchange",value = "exchange-dlx"),
                    @Argument(name = "x-dead-letter-routing-key",value = "dlx.ooo"),
                    @Argument(name = "x-message-ttl",value = "100000",type = "java.lang.Long"),
                    @Argument(name = "x-max-length",value = "3",type = "java.lang.Long"),
            }),
            exchange = @Exchange(name = "exchange-confirm",type = ExchangeTypes.TOPIC),
            key = "confirm.*"
    ))
    public void handlerMsg(String msg, Channel channel, Message message) throws IOException {
        try {
            //int i=1/0;
            System.out.println("消息:" + msg);
            // 消费者手动确认消息消费; 参数1:消息id;参数2:是否批量确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
        } catch (Exception e) {
            e.printStackTrace();
            // 需求:消息消费失败了,希望把消息重投到队列中,再次进行消费,如果还是消费失败就拒绝消息
            // 获取消息是否是重投的标记
            Boolean redelivered = message.getMessageProperties().getRedelivered();
            if (redelivered) {
                // 重投消息:可以拒绝不处理..
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
                System.out.println("消息重投后消费消息失败,拒绝处理");
            } else {
                // 未重投过:重投
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,true);
                System.out.println("消息消费异常,进行重投!");
            }
        }
    }
}

注:延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

3.3 怎么保证消息传递的顺序?

消息在投入到queue的时候是有顺序,如果只是单个消费者来处理对应的单个queue,是不会出现消息错乱的问题。但是在消费的时候有可能多个消费者消费同一个queue,由于各个消费者处理消息的时间不同,导致消息未能按照预期的顺序处理。其实根本的问题就是如何保证消息按照预期的顺序处理完成

出现消费顺序错乱的情况:
(1)为了提高处理效率,一个queue存在多个consumer:
在这里插入图片描述
(2)一个queue只存在一个consumer,但是为了提高处理效率,consumer中使用了多线程进行处理
在这里插入图片描述
保证消息顺序性的方法

(1)将原来的一个queue拆分成多个queue,每个queue都有一个自己的consumer。该种方案的核心是生产者在投递消息的时候根据业务数据关键值(例如订单ID哈希值对订单队列数取模)来将需要保证先后顺序的同一类数据(同一个订单的数据) 发送到同一个queue当中。
在这里插入图片描述
(2)
第一种:对于多线程的消费同一个队列的情况,可以使用重试机制:比如有一个微博业务场景的操作,发微博、写评论、删除微博,这三个异步操作。如果一个消费者先执行了写评论的操作,但是这时微博都还没发,写评论一定是失败的,等一段时间。等另一个消费者,先执行发微博的操作后,再执行,就可以成功。
第二种:一个queue就一个consumer,在consumer中维护多个内存队列,根据业务数据关键值(例如订单ID哈希值对内存队列数取模)将消息加入到不同的内存队列中,然后多个真正负责处理消息的线程去各自对应的内存队列当中获取消息进行消费。
在这里插入图片描述
RabbitMQ保证消息顺序性总结:
解决消费顺序问题,最根源的思路还是根据一定的策略,实现一个消费者按序处理一个队列中的消息。

核心思路就是根据业务数据关键值划分成多个消息集合,而且每个消息集合中的消息数据都是有序的,每个消息集合有自己独立的一个consumer。多个消息集合的存在保证了消息消费的效率,每个有序的消息集合对应单个的consumer也保证了消息消费时的有序性。

3.4 如何解决消息积压?

3.4.1 消息堆积原因

1.消息堆积即消息没及时被消费,是生产者生产消息速度快于消费者消费的速度导致的。
2.消费者消费慢可能是因为:本身逻辑耗费时间较长、阻塞了。即消费者消费失败,消费者出现性能瓶颈,消费者挂掉

3.4.2 消息积压的影响

可能导致新消息无法进入队列
可能导致旧消息无法丢失
消息等待消费的时间过长,超出了业务容忍范围。

3.4.3 解决办法
  1. 对生产者发消息接口进行适当限流(不太推荐,影响用户体验)
  2. 多部署几台消费者实例(推荐)
  3. 适当增加 prefetch 的数量,让消费端一次多接受一些消息(推荐,可以和第二种方案一起用)
  4. 增加消费者的多线程处理
  5. 死信队列

常见思路:
(1)使用Redis的List或ZSET做接收消息缓存,写一个程序按照消费者处理时间定时从Redis取消息发送到MQ在这里插入图片描述

(2) 设置消息过期时间,过期后转入死信队列,写一个程序处理死信消息(重新入队列或者即使处理或记录到数据库延后处理)
在这里插入图片描述
(3)拆分MQ,生产者一个MQ,消费者一个MQ,写一个程序监听生产者的MQ模拟消费速度(譬如线程休眠),然后发送到消费者的MQ,如果消息积压则只需要处理生产者的MQ的积压消息,不影响消费者MQ
在这里插入图片描述
(4)拆分MQ,生产者一个MQ,消费者一个MQ,写一个程序监听生产者的MQ,定义一个全局静态变量记录上一次消费的时间,如果上一次时间和当前时间只差小于消费者的处理时间,则发送到一个延迟队列(可以使用死信队列实现)发送到消费者的MQ,如果消息积压则只需要处理生产者的MQ的积压消息,不影响消费者MQ
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简兮冫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值