消息队列线程池模型如何保证重启时消息不丢

开发 前端
在很多消息队列中都有一个概念叫partion,代表着分区,分区是我们提高消息队列消费的关键,我们的消费者消费的渠道就是从每个分区中来的,一个分区只能被一个消费者持有。

 [[348392]]

本文转载自微信公众号「咖啡拿铁」,作者咖啡拿铁 。转载本文请联系咖啡拿铁公众号。

背景

今天在脉脉上面看到了一个帖子,比较有意思:

这个帖子的意思是:在使用Kafka的时候,我们已经设置了多个分区,如何去提升消费能力?如果使用线程池的方式去提升如何保证重启时消息不丢。

这个题其实问了两个点,第一个是如何提升消费能力,第二个是如果选择线程池,我们如何做到消息不丢。

这里先解释一下这两个问题到底是怎么回事,在很多消息队列中都有一个概念叫partion,代表着分区,分区是我们提高消息队列消费的关键,我们的消费者消费的渠道就是从每个分区中来的,一个分区只能被一个消费者持有,如下图所示:

有点类似银行排队,队列的个数越多,排队的时间相对来说就会越少,当然也可以通过异步的方式去处理,比如线程池,把所有的消息都扔到线程池中去执行,这就引出了作者说的第二个问题,首先我们来看看同步消费为什么不会丢消息呢?

如果我们使用的是同步模型,当我们消费了之后会将offset ack回去,如果我们出现了重启,没有成功offset,那么这部分数据将会再次消费,如果是用线程池进行消费,那么我们如何进行ack呢,比如我们用线程池消费了 10,11,12 三条消息如果12先消费完,那么我们ack 13吗?如果这样做的话,这个时候重启,kafka就会认为你已经处理了10,11的消息,这个时候消息就会出现丢失,而发这个帖子的同学就是对于这一块是比较疑惑。

网友的回答

我们来看看网友的一些回答:

网友A:

这名网友的回答本质还是使用线程池,作者也回复了,并没有解决线程池的问题。

网友B:

这个方法类似银行排队,只要队列多,那么处理速度就会加快,的确是第一个问题的解决办法之一。

网友C:

这一类主要解决了第二个问题,通过外部维护offset,比如通过offset入库的方式,我们就能找到正确的应该消费的offset,这个相对来说比较复杂,使用一个MQ还得配套一个数据库,万一我使用MQ的服务根本都没有数据库,还得单独去申请。

网友D:

还有另外一种观点就是,代码写好一点,让消费的速度提高,那消费能力自然就上去了,这个的确是一个很重要的点,通常被其他人给忽略,有时候消费比较慢,很多人可能一上来就是考虑中间件应该怎么设置,往往会忽略自己的代码。

看了这么多帖子的一个回复,感觉没有真正能让我满意的答案,下面来说说我心中的一些思路。

我的想法

对于第一个问题的话,如何提升消费能力?这个问题其实可以总结为三个办法:

  1. 如果每台消费者机器消费线程是固定的,那么我们可以扩容消费机器和partion,类似银行排队增加排队窗口一样。
  2. 如果机器和partion是固定的,增加消费线程就是一个比较好的办法,但是如果是顺序消费,就不能通过增加线程数的方式来提升消费能力,因为顺序消费每个partion都是一个单独的线程,只能通过第一种方式去解决。
  3. 增加自身代码的消费能力,你想想如果银行办事,如果柜员的办事效率能提升的非常高,那么整个排队速度肯定也是很快的。

对于第二个问题,如果我们使用线程池模型,如何去解决消息丢失问题,这里我比较推荐的是RocketMQ中的做法,我们之前说了用数据库去保存offset比较复杂,性能还比较差,在RocketMQ中使用了一个TreeMap的结构做了我们上面提到的数据库的事:

  1. private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>(); 

这个TreeMap的key是每个message的offset,value就是这条消息的一些信息,TreeMap的底层是使用红黑树去实现的,我们可以很快获取其中的最小值和最大值,当我们每次处理完某一条消息的时候我们会将这条消息从msgTreeMap中移除,

  1. public long removeMessage(final List<MessageExt> msgs) { 
  2.         long result = -1; 
  3.         final long now = System.currentTimeMillis(); 
  4.         try { 
  5.             this.lockTreeMap.writeLock().lockInterruptibly(); 
  6.             this.lastConsumeTimestamp = now; 
  7.             try { 
  8.                 if (!msgTreeMap.isEmpty()) { 
  9.                     result = this.queueOffsetMax + 1; 
  10.                     int removedCnt = 0; 
  11.                     for (MessageExt msg : msgs) { 
  12.                         MessageExt prev = msgTreeMap.remove(msg.getQueueOffset()); 
  13.                         if (prev != null) { 
  14.                             removedCnt--; 
  15.                             msgSize.addAndGet(0 - msg.getBody().length); 
  16.                         } 
  17.                     } 
  18.                     msgCount.addAndGet(removedCnt); 
  19.  
  20.                     if (!msgTreeMap.isEmpty()) { 
  21.                         result = msgTreeMap.firstKey(); 
  22.                     } 
  23.                 } 
  24.             } finally { 
  25.                 this.lockTreeMap.writeLock().unlock(); 
  26.             } 
  27.         } catch (Throwable t) { 
  28.             log.error("removeMessage exception", t); 
  29.         } 
  30.         return result; 
  31.     } 

removeMessage这个方法就是移除已经消费过的消息,并且返回当前最新的消费offset,这里返回的结果就是msgTreeMap.firstKey(),我们ack给消息队列server的值其实也是这个,回到我们这个问题上,如果我们发生重启,那么其实也不需要担心我们会出现消息丢失。

 

责任编辑:武晓燕 来源: 咖啡拿铁
相关推荐

2021-03-08 10:19:59

MQ消息磁盘

2024-08-06 09:55:25

2024-01-16 08:24:59

消息队列KafkaRocketMQ

2016-10-11 16:31:56

微信服务器消息

2016-11-02 13:12:31

微信离线消息

2021-10-22 08:37:13

消息不丢失rocketmq消息队列

2024-06-18 08:26:22

2020-03-12 09:34:05

Redis数据技术

2017-10-11 15:08:28

消息队列常见

2010-01-21 11:23:49

Linux多线程同步消息队列

2020-10-11 20:41:14

消息队列数据技术

2024-09-11 14:57:00

Redis消费线程模型

2024-05-09 08:04:23

RabbitMQ消息可靠性

2020-10-14 08:36:10

RabbitMQ消息

2024-12-18 07:43:49

2010-04-21 14:39:59

Unix消息队列

2024-03-22 12:10:39

Redis消息队列数据库

2016-11-10 21:00:49

消息存储数据

2020-10-18 07:25:55

MQ消息幂等架构

2021-02-02 11:01:31

RocketMQ消息分布式
点赞
收藏

51CTO技术栈公众号