Java程序员从笨鸟到菜鸟(五十四) 分布式之消息队列

作者明割
邮箱1311230692@qq.com

原文传送门https://www.cnblogs.com/rjzheng/p/8994962.html 博客讲的非常清晰易懂,非常感谢作者

##目录

该文只是一个复习思路,不了解消息队列的人建议先看《消息队列从入门到精通》


##一、为什么使用消息队列

三个最主要的应用场景解耦异步削峰

1、解耦

传统模式:
这里写图片描述
缺点:系统间的耦合性太强,如上图示,系统 A 在代码中直接调用系统 B 和系统 C 的代码,如果系统 D 接入,系统A还需要修改代码,过于麻烦!

中间件模式:
这里写图片描述
优点:将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统 A 不需要做任何修改

2、异步

传统模式:
这里写图片描述
**缺点:**一些非必要逻辑以同步方式运行,太耗费时间

中间件模式:
这里写图片描述
**优点:**将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

3、削峰
传统模式:
这里写图片描述
**缺点:**并发量大的时候,所有请求直接怼到数据库,造成数据库连接异常

中间件模式:
这里写图片描述
**优点:**系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的

##二、消息队列缺点
**分析:**一个使用了MQ的项目,如果连这个问题都没有考虑过,就把MQ引进去了,那就给自己的项目带来了风险。我们引入一个技术,要对这个技术的弊端有充分的认识,才能做好预防。要记住,不要给公司挖坑!

**缺点:**从以下两个角度来阐述

  • 系统可用性降低:本来其他系统只要运行好好的,你的系统就能正常运行,现在非要加消息队列进去,消息队列挂了,你的系统也就呵呵了
  • 系统的复杂性增加:要考虑多方面的问题,比如一致性问题,如何保证消息不被重复消费,如何保证消息的可靠性传输

##三、消息队列如何选型
先从以下四种 MQ 进行阐述:ActiveMQRabbitMQRocketMQKfaka

首先,先上ActiveMQ 社区,看该 MQ 的更新频率:

Apache ActiveMQ 5.15.3 Release
Christopher L. Shannon posted on Feb 12, 2018
Apache ActiveMQ 5.15.2 Released
Christopher L. Shannon posted on Oct 23, 2017
Apache ActiveMQ 5.15.0 Released
Christopher L. Shannon posted on Jul 06, 2017

我们可以看出,ActiveMq几个月才发一次版本,据说研究重心在他们的下一代产品Apollo.
接下来,我们再去RabbitMQ 社区,看该 MQ 的更新频率

RabbitMQ 3.7.3 release  30 January 2018
RabbitMQ 3.6.15 release  17 January 2018
RabbitMQ 3.7.2 release23 December 2017
RabbitMQ 3.7.1 release21 December 2017

相比较可以看出,RabbitMQ 版本发布比 ActiveMQ 频繁很多
再看一下性能对比表

特性ActiveMQRabbitMQRocketMQKafaka
开发语言javaerlangjavascala
单机吞吐量万级万级10万级10万级
时效性ms级us级ms级ms级
可用性高(主从架构)高(主从架构)非常高(分布式架构)非常高(分布式架构)
功能特性成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面丰富MQ 功能比较完备,扩展性佳只支持主要的 MQ 功能,像一些消息查询,消息回溯功能没有提供,毕竟为大数据准备,在大数据领域应用广

从上面表格可以得出结论:
1、中小型软件公司:建议选择 RabbitMQ,一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。正所谓,成也萧何,败也萧何!他的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
2、大型软件公司:根据具体使用在rocketMq和kafka之间二选一。一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。

##四、如何保证消息队列的高可用
分析:在第二点说过了,引入消息队列后,系统的可用性下降。在生产中,没人使用单机模式的消息队列。因此,作为一个合格的程序员,应该对消息队列的高可用有很深刻的了解。如果面试的时候,面试官问,你们的消息中间件如何保证高可用的?你的回答只是表明自己只会订阅和发布消息,面试官就会怀疑你是不是只是自己搭着玩,压根没在生产用过。请做一个爱思考、会思考、懂思考的程序员

要回答这问题,其实是要对消息队列的集群模式要有深刻了解,才更好进行阐述
RocketMQ 为例:它的集群有多 master 模式、多 master多 slave 同步双写模式、多 master 多 slave 异步复制模式。多 master 多 slave 模式部署架构图:
这里写图片描述

和 kafka 好像,只是 NameServer 集群,在 kafka 中是用 zookeeper 代替,都是用来保存和发现 master 和 slave 用的。通信过程如下:
Producer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Broker Master 建立长连接,且定时向 Broker 发送心跳。Producer 只能将消息发送到 Broker master,但是 Consumer 则不一样,它同时和提供 Topic 服务的 Master 和 Slave 建立长连接,既可以从 Broker Master 订阅消息,也可以从 Broker Slave 订阅消息。

kafka,为了对比直接上 kafka 的拓扑架构图:
这里写图片描述

如上图所示,一个典型的 kafka 集群中包含若干 Producer(可以是 web 前端产生的 Page View,或者是服务器日志,系统 CPU、Memory 等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。

##五、如何保证消息不被重复消费

为什么会造成重复消费?
其实无论是哪种消息队列,造成重复消费的原因都是类似的。正常情况下,消费者在消费消息时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。只是不同的消息队列发送的确认信息形式不同,例如 RabbitMQ 是发送一个 ACK 确认消息,RocketMQ 是返回一个 CONSUME_SUCCESS 成功标志,kafka 实际上有个 offset 的概念,简单说一下(如果还不懂,出门找一个 kafka 入门到精通教程),就是每一个消息都有一个 offset,kafka 消费过消息后,需要提交 offset,让消息队列知道自己已经消费过了。造成重复消费的原因:就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者

如何解决?针对不同的业务场景来回答:

  1. 如拿到这个消息做数据的 insert 操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
  2. 如拿到这个消息做 redis 的 set 的操作,那就容易了,不用解决,因为你无论 set 几次结果都是一样的,set操作本来就算幂等操作。
  3. 如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以 redis 为例,给消息分配一个全局 id,只要消费过该消息,将<id,message> 以 K-V 形式写入 redis。那消费者开始消费前,先去 redis 中查询有没消费记录即可。

##六、如何保证消费的可靠性传输

要保证可靠性传输,每种 MQ 都要从三个角度来分析问题:生产者丢数据消息队列丢数据消费者丢数据
RabbitMQ

1、生产者丢数据:
从这个角度来看,RabbitMQ 提供 transaction 和 confirm 模式来确保生产者不丢数据。transaction 机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。然而缺点就是吞吐量下降了。根据以往经验,生产上用 confirm 模式的居多。一旦 channel 进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID (从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ 就会发送一个 Ack 给生产者(包含消息的唯一 ID ),这就使得生产者知道消息已经正确到达目的队列了.如果 rabiitMQ 没能处理该消息,则会发送一个 Nack 消息给你,你可以进行重试操作。处理Ack和Nack的代码如下所示:

channel.addConfirmListener(new ConfirmListener() {  
    @Override  
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {  
        System.out.println("nack: deliveryTag = "+deliveryTag+" multiple: "+multiple);  
    }  
    @Override  
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {  
        System.out.println("ack: deliveryTag = "+deliveryTag+" multiple: "+multiple);  
    }  
}); 

2、消息队列丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ 阵亡了,那么生产者收不到 Ack 信号,生产者会自动重发。
如何持久化:

  1. 将 queue 的持久化标志 durable 设置为 true,则代表一个持久化的队列
  2. 发送消息的时候将 deliveryMode = 2

3、消费者丢数据
原因:消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时rahbitMQ会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息。
解决方案:采用手动确认消息即可

kafka

下图是一张 kafaka Replication 的数据流向图:
这里写图片描述

Producer 在发布消息到某个 Partition 时,先通过 ZooKeeper 找到该 Partition 的 Leader,然后无论该 Topic 的 Replication Factor 为多少(也即该 Partition 有多少个 Replica),Producer 只将该消息发送到该 Partition 的 Leader。Leader 会将该消息写入其本地 Log。每个Follower 都从 Leader 中 pull 数据。

1、生产者丢数据
在 kafka 生产中,基本都有一个 leader 和多个 follwer。follwer 会去同步 leader 的信息,因此,为了避免生产者丢数据,做如下两点配置:

  1. 第一个配置要在 producer 端设置 acks=all。这个配置保证了,follwer 同步完成后,才认为消息发送成功。
  2. 在 producer 端设置 retries=MAX,一旦写入失败,这无限重试

2、消息队列丢数据
针对消息队列丢数据的情况,无外乎就是,数据还没同步,leader 就挂了,这时 zookpeer 会将其他的 follwer 切换为 leader,那数据就丢失了。针对这种情况,应该做两个配置:

  1. replication.factor 参数,这个值必须大于1,即要求每个 partition 必须有至少 2 个副本
  2. min.insync.replicas 参数,这个值必须大于1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系
    这两个配置加上上面生产者的配置联合起来用,基本可确保 kafka 不丢数据

3、消费者丢数据
这种情况一般是自动提交了offset,然后你处理程序过程中挂了。kafka以为你处理好了。
offset:指的是 kafka 的 topic 中的每个消费组消费的下标。简单的来说就是一条消息对应一个 offset 下标,每次消费数据的时候如果提交 offset,那么下次消费就会从提交的 offset 加一那里开始消费。
比如一个 topic 中有 100 条数据,我消费了 50 条并且提交了,那么此时的 kafka 服务端记录提交的 offset 就是 49(offset从0开始),那么下次消费的时候 offset 就从 50 开始消费。
解决方案简单,改成手动提交即可

##七、如何保证消息的顺序性
针对这个问题,通过某种算法,将需要保持先后顺序的消息放到同一个消息队列中(kafka中就是 partition,rabbitMq 中就是 queue)。然后只用一个消费者去消费该队列。
如果为了吞吐量,有多个消费者去消费怎么办?
保证入队有序就行,出队以后的顺序交给消费者自己去保证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值