消息队列
消息队列是一个使用队列来通信的组件。随着互联网的快速发展,业务不断扩展,促使技术架构需要不断演进,从以前的单体架构到现在的微服务架构,成百上千的服务之间的相互调用和依赖。我们需要有一个东西来解耦服务之间的关系,控制资源合理合适的使用以及缓冲流量洪峰等等。
消息队列可以实现异步处理、服务解耦、流量控制。
异步处理
在我实际工作中,有几个场景使用到了消息队列的异步处理。 场景一 集群服务A是一个省的中心。单个服务小a用于售卖各地市。对于单个服务小a来说他要对一些记录进行上传省中心。为了不影响服务小a本身业务的影响,使用消息队列进行异步处理,提升系统总体性能。 场景二 对于一些日志使用消息队列进行异步保存以及消息通知第三方系统的业务都使用了消息队列来进行异步处理。 场景三 服务B调用服务C下单后,将订单traceId放入消息队列,消息队列异步处理来查询用户的订单状态。
总体来说,消息队列可以减少请求的等待,还能让服务异步并发处理,提升系统总体性能。
服务解耦
上游系统发现下游系统不断的扩充,为了避免不断的修改,一般选用消息队列来解耦系统之间的耦合问题。
流量控制
网关的请求先放入消息队列中,后端服务尽自己最大努力处理请求,消息队列能在其中发挥很好的削峰填谷的缓冲效果。
消息队列 RabbitMQ、rocketMQ、Kafaka对比
特 性 | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|
语言 | Erlang | Java | Scala |
单机吞吐 | 万 | 十万 | 十万 |
可用性 | 高(主从架构) | 非常高 (分布式架构) | 非常高 (分布式架构) |
管理界面 | 比较好 | 一般 | 一般 |
时效性 | us | ms | ms(以内) |
功能特性 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低,管理界面较丰富,支持多语言 | MQ功能比较完备,扩展性佳。仅适用于JAVA语言 | 只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
适用场景 | 数据量没有那么大,小公司 | 目前RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。 | 适用于埋点上报,日志采集,大数据流计算。 |
Broker端消息过滤 | 不支持 | 可以支持Tag标签过滤和SQL表达式过滤 | 不支持 |
消息查询 | 根据消息id查询 | 支持Message Id或者Key查询 | 不支持 |
消息回溯 | 不支持 | RocketMQ支持按照时间来回溯消息,精度毫秒,例如从一天之前的某时某分某秒开始重新消费消息 | 理论上可以支持时间或offset回溯,但是得修改代码 |
路由逻辑 | 基于交换机,可配置复杂路由逻辑 | 根据topic,可以配置过滤消费 | 根据topic |
持久化 | 队列基于内存,只能少量堆积 | 大量堆积 | 大量堆积 |
顺序消息 | 不支持 | 支持 | 支持 |
消费并行度 | 无影响 | ●顺序消费方式并行度同Kafka完全一致 ●乱序方式并行度取决于Consumer的线程数,如Topic配置10个队列,10台机器消费,每台机器100个线程,那么并行度为1000。 | Kafka的消费并行度依赖Topic配置的分区数,如分区数为10,那么最多10台机器来并行消费(每台机器只能开启一个线程),或者一台机器消费(10个线程并行消费)。即消费并行度和分区数一致 |
RocketMQ
工作中我们经常使用前辈写好的工具类进行发送接收,RocketMQ架构流程还是值得学习的,甚至可以学习他的源码,因为他的源码的Java。这次主要记录一下主要流程。具体的细节可以去看文末博客。
整体架构
Broker启动后会向所有NameServer定期(30S)发送心跳包以及ip、port、topicInfo等信息。
NameServer会定期扫描Broker存活列表,如果超过120S没有心跳移除此Broker相关信息。
这样的话NameServer就知道Broker的相关消息了,就好提供给生产者以及消费者。
Producer发送消息流程
Producer每30s会向NameServer拉取路由信息更新本地路由表,有新的Broker就和其建立长连接,每隔30s发送心跳给Broker。
生产者使用同步消息发送、异步消息发送,会有Broker是否发送成功的响应,做好try-catch处理响应。如果是失败和异常响应进行重试发送。多次失败后可以进行日志记录来保证生产者在生产消息阶段消息不会丢失。
从图中可以看出一个topic会有多个Queue,Queue还有可能在不同的broker上。正常的情况会采取round robin轮询方式把消息发送到不同的queue,我们也可以指定发送消息的时候去指定分区或一个分区算法。
RocketMQ的延迟消息是指的是发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。有18个level。延迟消息的原理是在发送消息时,发现是延迟消息,就会替换topic,将消息暂存在名为schedule_topic_XXX的topic中,并根据delayTimeLevel存入特定的queue。brocker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真是的Topic。 实际项目中我们会根据重试的次数来进行计算延迟消息,重试的次数越多延迟越久。比如用户半个小时内还未支付订单,进行到第5次还未支付就会延迟1个小时再来查询,第六次就直接当失败处理,关闭订单。
事务消息我会再另一篇总结笔记,因为我觉得他跟MySQL事务、分布式事务是由联系的。
Brocker存储
commotLog采用混合型存储,也就是所有Topic都存在一起,顺序追加写入,文件名用起始偏移量命名。
消息先写入commitlog再通过后台线程分发到consumerQueue和indexFile中。
消费者先读取consumerQueue得到真正的物理地址,然后访问commitLog得到真正的消息。
使用主从的方式保证数据可靠性,开启异步刷盘和主从同步复制来保证broker端的可用性。 极端情况,mq还没有进行持久化,就挂掉了。或者由于网络原因之类的,生产者收不到confirm。我们对于极其重要的消息就要做数据入库。例如,生产者要发送一条消息之前,先插入数据库,status标识设置为0。收到回调成功之后设置为1。通过定时任务来定期扫描重发,进行消息的补偿。要设置最大重试次数。这种方式会加到数据库交互的压力,建议也可以直接通过在缓存中操作状态的变更,数据库定时清扫,减少压力,视我们的实际情况而定。
Consumer消费者
广播模式:一个分组下的每个消费者都会消费完整的Topic消息。 集群模式:一个分组下的消费者瓜分消费Topic消息。(线上使用这种方式以及拉模式) push:服务端主动给客户端推送,实时性高。 pull: 客户端想消费多少就消费多少,不存在占用消费者资源的问题。 生产上采用拉模式,消息队列本身有持久化消息的需求,本身有存储的功能具体分析可以看公众号《yes的练级攻略》的消息队列那些事儿的专栏,写的通俗易懂。
消费者和生产者都可以给消息打上标签,这样的话会在broker端进行过滤减少无用的消息在网络中传输的成本。
至于消息重试rocketMQ会为每个消费组都设置一个Topic名称为"%RETRY%+consumerGroup"的重试队列用于暂时保存因各种异常而导致consumer端无法消费的消息。重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为"SCHEDULE_TOPIC_XXX"的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至"%RETRY%+consumerGroup"的重试队列中。
消息消费失败的原因如果是类似因为下游系统网络不可达的情况下,遇到这种就要跳过,而不是重试。
在我们项目中我们是这样进行项目重试
每个消费者都会继承这个抽象类,具体业务具体实现,根据具体的业务明确返回的状态来进行重试或者记录异常。消费者常常是跟分布式锁一起关联起来的。保证同一时间只有一个消费者来消费其逻辑。
完全消费正常后在进行手动ack确认。消费者从broker拉取消息,然后执行相应的业务逻辑。一旦执行成功,将会返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS这个状态给brocker。
在消息消费时,要求消息体中必须要有一个bizId,对于同一业务全局唯一,比如traceid作为去重和幂等的依据,避免同一条消息被重复消费。 业务场景1:拿到这个消息去数据库做insert,如果库里有,就会导致主键冲突。 业务场景2:利用redis分布式锁+消息唯一id去查订单状态如果是明确的状态就直接返回。 业务场景3:拿到这个消息做redis的set操作,更容易,不用解决,set操作本来就算是幂等的。 业务场景4:利用redis布隆过滤器,拿到这个消息去布隆过滤器里面查询是否存在。
鸣谢
从这几篇博客中收获很大并梳理成自己的笔记,分享给大家 CSDN-RocketMQ专栏 微信公众号《yes的练级攻略》消息队列专栏