消息队列的基本功能
消息队列作为系统解耦,流量控制的利器,成为分布式系统核心组件之一。在日常的开发我们享受了使用消息队列带来的便利,那么如果要自己实现一个消息队列应该入手。本文不深入讨论具体,成熟的消息队列如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的优点的就是主动权掌握在消费方,可以根据自己的消息速度进行消息拉取,缺点就是消费方不知道什么时候可以获取的最新的消息,会有消息延迟和忙等。