消息队列的基本功能

消息队列作为系统解耦,流量控制的利器,成为分布式系统核心组件之一。在日常的开发我们享受了使用消息队列带来的便利,那么如果要自己实现一个消息队列应该入手。本文不深入讨论具体,成熟的消息队列如kafka,rocketmq等,主要介绍一下基本功能,思想和设计。
首先转换一下角色,作为产品经理给自己提出一个实现消息队列的需求,那么首先列一下消息队列必备的功能有哪些。

  • 消息堆积
  • 消息持久化
  • 消息顺序
  • 消息最少投递一次
  • 支持多topic
  • 相同topic支持多consumer
  • 消息回溯
  • 集群功能
  • 负载均衡

一个简单的消息队列基本功能如上,在某些特殊的场景还需要支持事务,消息重试等功能。此外除了功能部分,还需要尽可能优化性能,提供监控功能帮助报警和排查问题。

消息队列的设计实现

明确了功能需求,接下来就要考虑如何实现一个消息列队。
消息队列主要涉及到三个部分,通信协议+存储+消费关系维护。

通信协议

极简版的消息队列甚至只需要一个redis就可以,按照topic将序列化的数据存储到redis,消费端使用redis的incr功能获取锁,获取到锁的consumer不断的轮询获取消息。当然这种极简版的消息队列是不能通过复杂的生产环境检验的,系统的可靠性也不能保证。这里将极简版的消息队列升级一下,使用成熟的RPC框架实现通信协议,将一次同步RPC调用变成2次PRC+存储,PRC框架帮我们解决了负载均衡,服务发现,通信协议,序列化/反序列化等问题。同时RPC框架也保证了通信层面的高可用。

存储选择

对于分布式系统,存储的选择有以下几种

  • 内存
  • 本地文件系统
  • 分布式文件系统
  • nosql
  • rdbms
    从速度上内存显然是最快的,对于允许消息丢失,消息堆积能力要求不高的场景(例如日志),内存会是比较好的选择。rdbms则是最简单的实现可靠存储的方案,很适合用在可靠性要求很高,最终一致性的场景(例如交易消息),对于不需要100%保证数据完整性的场景,要求性能和消息堆积的场景,hbase也是一个很好的选择。
消费关系

在消息存储在broker上后,需要的就是将消息正确的投递到消费者,消息的投递分为广播和单播, 最常见的使用场景是组内单播,组间广播,同一个集群使用相同的group注册订阅,通常消息队列本身不维护消费订阅关系,使用例如zookeeper等成熟的系统维护消费关系,在消费关系发生变化时下发通知。

队列特性

确定了消息队列的模块功能需求后,还需要考虑队列的特性需求,这里重点考虑消息丢失,消息确认,消息重复,消息顺序性,投递方式

消息丢失

消息丢失可能发生在3种情况

  • 生产者-> 队列
  • 队列-> 消费者
  • 队列持久化本身
    在生产者产生消息到队列的过程可能由于网络问题,宕机等原因没有达到消息队列,或者到达队列后并没有返回消息,这时可以通过重试等方式解决。队列到消费者时也会存在同样的问题,可以通过增加一个标识,投递消息前先标记成待完成状态,在收到消费者确认成功的回复后标记成完成。可以看到消息丢失和消息重复是一个硬币的2面, 保证消息不丢失也会带来消息重复的问题。
消息确认

消息确认就是将2次RPC变成3次RPC。在某些场景默认auto ack是可以的,但也需要支持消息者主动ack,在之后的某个时间重新投递

消息重复

上面提到,在需要保证消息不丢失的场景,因为对投递失败的情况不可确定是失败,还是超时,需要进行重发,一定会带来消息的重复,这里要考虑的是如何减少重复,可以根据一定的规则(业务id+业务名),数据库唯一主键,分布式主键等产生messageId(为了方便排查问题,一定要有messageId), 对比清除周期内记录的messageId,如果messageId相同就认为是重复的消息。

消息顺序性

比较简单有效的实现消息顺序性的方式就是单线程生产者+单线程消费者+每个消费线程对应一个单独队列, 排除消息丢失的情况,可以做到严格有序。

投递方式

消息队列的投递方式可以分为push和pull2种,一种模型的某些场景下的优点,在另一些场景就可能是缺点。无论是push还是pull,都存在各种的利弊。
push的优点就是及时性,缺点就是受限于消费者的消费能力,可能造成消息的堆积,broker会不断给消费者发送不能处理的消息。
pull的优点的就是主动权掌握在消费方,可以根据自己的消息速度进行消息拉取,缺点就是消费方不知道什么时候可以获取的最新的消息,会有消息延迟和忙等。