消息队列初见:一起聊聊引入系统mq 之后的问题

一:为什么使用MQ

中小公司系统一开始体量较小,后来随着用户量逐渐扩大,需要把很多业务进行拆分,因此引入消息队列来优化问题。

其通用的使用场景可以简单地描述为:

当不需要立即获得结果,但是并发量又需要进行控制的时候,就是需要消息队列的时候。

二:消息队列应用的几大场景:

解耦

多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败。

具体场景:用户注册,系统发送邮件并验证短信。

一般情况下,串行方式,并行方式均可。

image

或使用消息队列,注册信息写入到消息队列。

或电商交易系统,交易服务发送消息,要生成订单,要扣减库存,要物流发货。
image

异步

多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间。

具体场景:人脸识别系统。用户上传图片,调用人脸识别,调用完成后再返回信息。该方法的缺点是:1 人脸识别失败导致图片上传失败;2 延迟高,需要系统处理完成后再返回客户端,即使用户不需要立即知道结果;3 图片上传系统和人脸识别系统互相调用,耦合度高。

image

客户端上传图片后,图片上传系统将图片信息如uid, 批次写入mq, 直接返回成功。人脸识别系统定时从mq 中取数据,完成对新增图片的识别。

此时,图片上传系统不需要关心人脸识别系统是否对这些图片信息的处理,以及何时对这些图片的处理。事实上,由于用户并不需要立即知道人脸识别的结果,人脸识别系统可以选择不同的调度策略,按忙时、闲时、正常时间对队列中的图片信息进行处理。

消峰

广泛应用于秒杀或抢购活动,避免流量过大导致系统挂掉。

具体场景:秒杀活动一般瞬时访问量过大,服务器接收过大,会导致流量暴增,相关系统无法处理请求甚至崩溃。而加入mq 后,系统可以从mq 中取数据,相当于mq 做了一次缓冲。

优点:

  • 1 请求先入mq, 而不是由业务系统直接处理,做了一次缓冲,极大减少业务处理系统的压力。
  • 2 队列长度可以做限制,事实上,秒杀时,后入队列的用户无法秒杀到商品,这些请求可以直接被抛弃,返回活动已结束或商品已售完。

消息驱动的系统

系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,多个消费者负责对消息进行处理。

具体场景:用户新上传一批照片,人脸识别系统需要对这个用户的所有照片进行聚类,聚类完成后由对帐系统重新生成用户的人脸索引加快查询。这三个子系统间由mq 连接起来,前一个阶段处理结果放入队列中,后一个阶段从队列中获取消息继续处理。

image

优点:

  • 避免了直接调用下一个系统导致当前系统失效
  • 每个子系统对于消息的处理方式可以更灵活,可以选择接收到消息 时就处理,可以选择定时处理,也可以划分时间段按不同处理速度处理。

三:MQ 存在问题及场景优化

3.1 重复消费问题

问题出现的场景:

  • 消息生产者产生了重复的消息
  • kafka 和 RocketMQ 的 offset 被回调了
  • 消息消费者确认失败
  • 消息消费者确认超时
  • 业务系统主动发起重试
解决方案

不管是生产者产生的重复消息,还是消费者导致的重复消息,我们都可以在消费者中解决这个问题。这就需要我们做幂等设计。

增加一张消费消息表,使用messageId 做唯一索引。在处理逻辑前,先根据messageId 查询一下该消息有没有处理过,如果已经处理过则直接返回,如果没有处理过,则继续做业务处理

3.2 数据一致性问题(异步分布式事务问题)

问题出现的场景:

当服务是同步调用的时候,我们可以使用本地事务来控制数据一致性。但异步调用会存在数据不一致的问题。如商品订单下单成功,但是扣减库存失败,就是造成超卖问题。

解决方案

数据一致性分为:强一致性,弱一致性,最终一致性。

mq 为了性能使用的是最终一致性,那么必定会出现数据不一致的问题。这类问题大概率是因为消费者读取消息后,业务逻辑处理失败导致的。这个时候可以增加重试机制。

重试可以分为同步重试和异步重试。消息量比较小的业务场景,可以采用同步重试,如果消息处理失败立即重试3-5次,如果还失败则写入记录表中。如果消息量比较大,则采用异步方式。在处理失败后立即写入重试表,有个job 专门重定时重试。

image

还有一种做法,如果消费失败,自己给同一个topic 发一条消息,在后面的某个时间点自己又会消费到那一条消息,起到了重试的效果。如果对消息顺序要求不高的场景。这种做法适合对消费顺序要求不高的场景。

需要事务强一致的,不用消息异步,如下单、减库存要放在一个事务时在,加积分等业务放在mq 中异步处理。

3.3 消息丢失问题

问题出现的场景:

  • 生产者产生消息时,由于网络原因发送到mq 失败
  • mq 服务器持久化,存储磁盘时出现异常
  • kafka 和 rocketMQ 的offset 被回调时,略过了很多消息
  • 消费者刚读取消息,已经ack 确认,但是业务还没处理完,服务被重启了。

生产者、消费者、服务器都可能产生问题,最终的结果会导致消费者无法正确的处理消息而导致数据不一致的情况。

解决方案

增加一张消息发送表。

  • 当生产者发完消息后,会往该表中写入一条数据,状态status 标记为待确认
  • 如果消费者读取消息后,调用生产者api 更新该消息status 为已确认
  • 有个job 每隔一段时间检查一次消息发送表,如果执行后还有状态是待确认的消息,则认为该消息丢失,重发该消息。

image

3.4 消息顺序问题

问题出现的场景:
如商品订单-支付-完成-退货等状态,如果订单数据作为 消息体,就会涉及顺序问题。如果先支付后下单,是不可以的。

这样的问题场景有很多,比如:

  • kafka 同一个partition 中能保证顺序,但是不同的partition 无法保证顺序
  • rabbitMQ 的同一个queue 能够保证顺序,但是如果多个消费者同一个queue 也会有顺序问题
  • 如果消费者使用多线程消费消息,无法保证顺序
  • 如果生产者发送到mq 中的路由规则跟消费者不一样,也无法保证顺序
解决方案

首先我们需要确认,消费者是否真的需要知道中间状态,只知道最终状态行不行。

image

其实很多时候我们真的需要知道的最终状态,这样我们可以把流程优化一下:

image

这种流程可以解决大部分的消息顺序问题。但是如果真的有需要保证顺序的需求,可以将订单号路由到不同的partition,同一个订单号的消息,每次发送到同一个partition

3.5 消息堆积问题

问题出现的场景:

很多时候,由于某些批处理或其他原因,导致消费速度小于生产速度,这样会直接导致消息堆积问题,从而影响业务功能。

比如开通会员,如果消息出现堆积会导致用户下单后,很久才变会员。这种情况肯定会引起大量用户投诉。

解决方案

看消息是否需要保证顺序。如果不需要保证顺序,可以读取消息之后用多线程处理业务逻辑。

这样就能增加业务逻辑处理速度,解决消息堆积问题。但是线程池的核心线程数和最大线程数需要合理配置,不然可能会浪费系统资源。

image

如果需要保证顺序,可以读取消息后将消息按照一定的规则分发到多个队列中,然后在队列中用单线程处理。

image

rabbitmq ,activemq , rocketmq, kafka 对比

image

引入mq 之后的问题

本来我们需要保证系统服务可用就可以了,现在引入新的服务,我们要保证mq 的可用,系统的可用性和复杂性都会有新的要求。

文::一只阿木木

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章