RabbitMQ 消息幂等性&顺序性&消息积压&面试问题

RabbitMQ 消息幂等性&顺序性&消息积压&面试问题

幂等性概念

幂等性,简单来说就是对于同一个系统,在同样条件下,一次请求和重复多次请求对资源的影响是一致的,就称该操作为幂等的。比如说如果有一个接口是幂等的,当传入相同条件时,其效果必须是相同的。在RabbitMQ中消费幂等就是指给消费者发送多条同样的消息,消费者只会消费其中的一条,避免出现重复消费的问题

通俗的讲就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错;类似于数据库中的乐观锁机制,如果更新数据库中的一条SQL;在并发场景,为了性能和数据可靠性,会在更新时加上查询时的版本,并且更新这个版本信息

RabbitMQ的幂等性

出现非幂等性的情况:

  1. 生产者已把消息发送到mq,在mq给生产者返回ack的时候网络中断,故生产者未收到确定信息,生产者认为消息未发送成功,但实际情况是,mq已成功接收到了消息,在网络重连后,生产者会重新发送刚才的消息,造成mq接收了重复的消息

    image-20220323135742940

  2. 消费者在消费mq中的消息时,mq已把消息发送给消费者,消费者在给mq返回ack时网络中断,故mq未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息

image-20220323140724863

解决幂等性问题方案

解决幂等性的唯一目的就是确定消息的唯一性的一个状态,既不会重复发送,也不会重复消费。

方案一(借鉴mysql乐观锁机制)

借鉴Mysql的乐观锁机制主要分为几个步骤:

  • 创建消息记录表,并且添加version字段
  • 发送消息入库一条数据,设置状态,以及版本号
  • 分布式下,如果其他系统再次消费相同消息时,以版本号为条件,条件不满足,着无法继续消费,通过mysql的乐观锁机制来保障消息的幂等性

方案二(消费端端幂等性)

幂等性问题再消费者端,比较常见,所有在MQ系统内部必须生成一个inner-msg-id,作为去重和幂等的依据,这个内部消息ID的特性是:

(1)全局唯一

(2)MQ生成,具备业务无关性,对消息发送方和消息接收方屏蔽

通过全局唯一ID 就可以实现生产者端到MQ中间件 一个消息对应唯一一个ID

1. 唯一 ID + 指纹码 机制,利用数据库主键去重

  • 唯一ID :就是消息的唯一ID
  • 指纹码:通过规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中。好处就是实现简单,就一个拼接,然后查询判断是否重复。

优点:

  • 实现简单,直接是通过代码的拼接Key 到数据库中查重

缺点:

  • 有比较大的并发量的限制,资源消耗大,写入性能瓶颈

优化缺点:

  • 根据 ID 进行分库分表,对 id 进行算法路由,落到一个具体的数据库,然后当这个 id 第二次来又会落到这个数据库,这时候就像我单库时的查重一样了。利用算法路由把单库的幂等变成多库的幂等,分摊数据流量压力,提高性能。

2. 利用redis的原子性去实现

利用redis的原子性,首先考虑:

  • 是否 要进行数据落库

    • 如果落库,关键解决的问题是数据库和缓存如何做到原子性,一致性? 数据库与缓存进行同步肯定要进行写操作,到底先写 redis 还是先写数据库,这是个问题,涉及到缓存更新与淘汰的问题

      • 写入数据,redis具有天然的幂等性,set执行即可
      • 数据库的话,先根据主键查,如果存在着update不在插入
    • 如果不落库,那么都存储到缓存中,如何设置定时同步的策略? 不入库的话,可以使用双重缓存等策略,保障一个消息副本,具体同步可以使用类似 databus 这种同步工具

  • 更复杂的场景:

    • 基于redis的,生产者参数唯一ID发送消息,消费者拿到消息先到redis中查,查到表示已经消费,不在执行业务代码
    • 基于数据库的唯一主键,重复插入异常

消息的顺序性

业务需求场景

  • mysql binlog同步的系统,日同步数据要达到上亿,压力比较大 mysql -> mysql然后对公司的业务系统的数据做各种复杂的操作
  • 此时我们在mysql(主里面做 ins-->update-del) 新增,修改,删除操作,最后数据被删除,此时对应的是3条binglog记录发送到MQ中
  • 那么消费这三条记录就需要保证一定的顺序性,否则很可能同步的结果不相同,数据就乱了,本来应该删除的数据最后保留了

错乱的原因

下图【多个消费者消费,无法保证消息的顺序性】

img

顺序性解决方案

rabbitmq:拆分多个queue,每个queue对应一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

img


消息积压问题

在大型分布式项目中,如果消费者挂了,无法消费消息,此时就有可能出现MQ中挤压大量,千万级消息。

业务场景

先假设一个场景,我们现在消费端出故障了,然后大量消息在mq里积压

消息积压解决方案

此时就需要最紧急的问题就是,临时紧急扩容大概思路:【坑一】

  • 先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉
  • 新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量
  • 然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
  • 接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据
  • 这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据
  • 等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息

【坑二】

rabbitmq设置了TTL过期时间,如果消息在queue中积压超过一定的时间就会被rabbitmq给清理掉,现在的问题就不是消息挤压,而是直接变成大量消息丢失

这个时候,最终的解决方案,一般都是批量重新导入MQ。手动补偿

相关的面试题

参考:【https://www.cnblogs.com/hello-/articles/10345021.html】

生产者弄丢了数据

生产者将数据发送到rabbitmq的时候,可能数据就在半路给搞丢了,因为网络啥的问题,都有可能。此时可以选择用rabbitmq提供的事务功能,就是生产者发送数据之前开启rabbitmq事务(channel.txSelect),然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)。但是问题是,rabbitmq事务机制一搞,基本上吞吐量会下来,因为太耗性能。

所以一般来说,如果你要确保说写rabbitmq的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息rabbitmq接收了之后会异步回调你一个接口通知你这个消息接收到了。

所以一般在生产者这块避免数据丢失,都是用confirm机制的。


rabbitmq弄丢了数据

就是rabbitmq自己弄丢了数据,这个你必须开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。

设置持久化有两个步骤,第一个是创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的,此时rabbitmq就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,rabbitmq哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。

而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。

哪怕是你给rabbitmq开启了持久化机制,也有一种可能,就是这个消息写到了rabbitmq中,但是还没来得及持久化到磁盘上,结果不巧,此时rabbitmq挂了,就会导致内存里的一点点数据会丢失。


消费端弄丢了数据

rabbitmq如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,rabbitmq认为你都消费了,这数据就丢了。

这个时候得用rabbitmq提供的ack机制,简单来说,就是你关闭rabbitmq自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。


消息如何刷到磁盘?

  • 写入文件前会有一个Buffer,大小为1M,数据在写入文件时,首先会写入到这个Buffer,如果Buffer已满,则会将Buffer写入到文件(未必刷到磁盘)。
  • 有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每个25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘。
  • 每次消息写入后,如果没有后续写入请求,则会直接将已写入的消息刷到磁盘:使用Erlang的receive x after 0实现,只要进程的信箱里没有消息,则产生一个timeout消息,而timeout会触发刷盘操作。

设计消息队列

  1. 首先这个mq得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下kafka的设计理念,broker -> topic -> partition,每个partition放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
  2. 其次你得考虑一下这个mq的数据要不要落地磁盘吧?那肯定要了,落磁盘,才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是kafka的思路。
  3. 其次你考虑一下你的mq的可用性啊?这个事儿,具体参考我们之前可用性那个环节讲解的kafka的高可用保障机制。多副本 -> leader & follower -> broker挂了重新选举leader即可对外服务。

生产者如何获取到没有被正确路由到合适队列的消息

可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。RabbitMQ 会通过 Basic . Return 返回 “mandatory test” 这条消息,之后生产者客户端通过 ReturnListener 监昕到了这个事 件,上面代码的最后输出应该是”Basic.Retum 返回的结果是: mandatory test”


mandatory和immediate参数的区别

mandatory 参数告诉服务器至少将该消息路由到一个队列中, 否则将消息返 回给生产者。 immediate 参数告诉服务器, 如果该消息关联的队列上有消费者, 则立刻投递: 如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者, 不用将消息存入队列而等 待消费者了。


未被路由到的消息应该怎么处理

发送消息的时候设置mandatory参数,添加ReturnListener监听器接收未被路由到的返回消息
采用备份交换器AE,可以将未被路由的消息存储在RabbitMQ中,通过声明交换器的时候添加AE参数实现,或者通过策略的方式实现,同时使用,前者优先级高,会覆盖掉Policy的设置


备份交换器需要注意

  • 如果设置的备份交换器不存在,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失
  • 如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失
  • 如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失
  • 如果备份交换器和mandatory参数一起使用,那么mandatory参数无效

怎么为消息设置过期时间TTL

  • 通过队列属性设置,队列中所有消息都有相同的过期时间,声明队列的时候在channel.queueDeclare加入TTL参数
  • 对消息本身进行单独设置,每条消息的TTL可以不同,在channel.basicPublish方法参数中设置
  • 同时使用以上两种方式设置过期时间,以较小的为准
  • 消息在队列中的生存时间一旦超过设置的TTL值,就变成死信,消费者无法再收到该消息(不是绝对的)
  • 如果不设置 TTL.则表示此消息不会过期;如果将 TTL 设置为 0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃,这个特性可以部分替代 RabbitMQ 3.0 版本之前的 immediate 参数

对过期消息处理

  • 设置队列 TTL 属性的方法,一旦消息过期,就会从队列中抹去,队列中己过期的消息肯定在队 列头部, RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可,
  • 消息本身进行单独设置,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的。每条消息的过期时间不同,如果要删除所有过期消息势必要扫描整个队列,所不如等到此消息即将 被消费时再判定是否过期, 如果过期再进行删除即可。

设置队列的过期时间

  • 通过 channel . queueDeclare 方法中的 x-expires 参数可以控制队列被自动删除前处 于未使用状态的时间。未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未调用过 Basic . Get 命令。
  • RabbitMQ 会确保在过期时间到达后将队列删除,但是不保障删除的动作有多及时 。在 RabbitMQ 重启后,持久化的队列的过期时间会被重新计算。

什么是死信队列

  • DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
  • DLX 也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定, 实 际上就是设置某个队列的属性。当这个队列中存在死信时 , RabbitMQ 就会自动地将这个消息重新发布到设置的 DLX 上去,进而被路由到另一个队列,即死信队列。

什么是延迟队列

  • 延迟队列存储的对象是对应的延迟消息,所谓“延迟消息”是指当消息被发送后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费

延迟队列应用场景

  • 订单系统,用延迟队列处理超时订单用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将 用户指令发送到延迟队列,当指令设定的时间到了再将指令推送到智能设备。
    持久化?

交换器的持久化

交换器的持久化是通过在声明交换器时将 durable 参数置为 true 实现的,如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换器元数据会丢失, 不过消息不会丢失,只是不能将消息发送到这个交换器中了。对一个长期使用的交换器来说,建议将其置为持久化的。


队列的持久化

队列的持久化是通过在声明队列时将 durable 参数置为 true 实现的,如果队列不设置持久化,那么在 RabbitMQ 服务重启之后,相关队列的元数据会丢失,此时数据也会丢失


消息的持久化

通过将消息的投递模式 (BasicProperties 中的 deliveryMode 属性)设置为 2 即可实现消息的持久化


消费端对消息的处理

  • 过推模式或者拉模式的方 式来获取井消费消息,当消费者处理完业务逻辑需要手动确认消息己被接收,这RabbitMQ才能把当前消息从队列中标记清除
  • 如果消费者由于某些原因无法处理当前接收到的消息, 可以通过 channel . basicNack 或者 channel . basicReject 来拒绝掉。

消息分发

  • 同一个队列拥有多个消费者,会采用轮询的方式分发消息给消费者,若其中有的消费者任务重,有的消费者很快处理完消息,导致进程空闲,这样对导致整体应用吞吐量下降,为了解决上面的问题,用到channel.basicQos 方法允许限制信道上的消费者所能保持的最大未确认消息的数量。Basic.Qos 的使用对于拉模式的消费方式无效.

举例如下:

  • 在订阅消费队列之前,消费端程序调用了 channel.basicQos(5) ,之后订 阅了某个队列进行消费。 RabbitMQ 会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,如果达到了所设定的上限,那么 RabbitMQ 就不会向这个消费者再发送任何消息。 直到消费者确认了某条消息之后 , RabbitMQ将相应的计数减1,之后消费者可以继续接收消息, 直到再次到达计数上限。这种机制可以类比于 TCP!IP中的”滑动窗口”。

RabbitMQ学习系列

RabbitMQ 安装快速下载

springboot简单整合RabbitMQ

RabbitMQ消息队列

RabbitMQ 消息投递以及ACK机制

RabbitMQ 消息幂等性&顺序性&消息积压&面试问题

posted @ 2022-03-24 17:09  Mr*宇晨  阅读(687)  评论(0编辑  收藏  举报