一、市面上流行的消息队列对比
- ActivityMQ activityMQ是老牌的消息队列,技术相对成熟,但吞吐量一般,目前行业趋势渐渐用的少了,社区也相比其他mq不够活跃。
- RabbitMQ,rabbitMQ是基于erlang语言开发的,国内中小企业比较流行,功能完备,特别值得一提的是管理后台界面足够人性化,功能丰富。并且社区活跃度较高,吞吐量还行。
- RocketMQ 吞吐量比RabbitMq高,阿里开发,基于java语言,功能强大。
- kafkaMQ 吞吐量比rocketMQ还要高,架构足够轻,易于扩展,是大数据实时计算、日志采集领域使用消息中间件的标准,但功能较少,并没有"事务性""消息传输担保(消息确认机制)""消息分组"等企业级特性。
总结看来,技术选型中,activityMQ一般不推荐,中小型公司用rabbitMQ(功能完备,使用较简单,有很好的后台界面)。 rocketMQ适合大型公司,大数据领域适合用kafka.
二、为什么使用消息队列
1. 解耦
相比调用而言,消息队列有发布订阅模式(PUB/SUB),原来场景中的调用方只需要发送消息,不用考虑谁消费,不用手动维护调用关系,如果需要数据的系统太多维护起来是很复杂的。
2. 异步
消息队列常用于异步场景,以此减少响应时间。
3. 削峰
应用消息队列的拉模式能够很好的缓解系统在高峰期的压力,消费者保持自己能够接收限度去拉取消息。
三、消息队列可能导致的问题
1.可用性问题(MQ崩溃则整个系统崩溃)
以rabbitMQ为例,rabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式。
单机模式
只有一个mq服务节点,没有可用性可言
普通集群模式
一个queue不会存多个副本在每个节点,真实数据只保存在一个节点上,但其他节点会保存该queue的元数据元(存储队列指针,长度等),消费者可以通过集群中的任何节点获取到数据(访问的节点通过元数据取到实际存储队列的节点的数据然后返回给消费者)。但真实数据只在一个节点中,如果配置了持久化,那么实际保存数据的节点蹦了,其他节点不能创建同样的队列,所以不符合高可用性。这种模式优点只是在于分散mq的cpu,内存压力,提高数据存储空间。但也会存在很多集群内部各个节点为了传输消息的网络IO,同时可用性没有保障。
镜像集群模式
相对普通集群模式,所有节点同步保存队列的真实数据。管理控制台可以配置一个镜像集群策略,指定所有节点或指定数量节点同步节点队列。缺点是降低了系统性能,节点每次同步数据的网络开销大。master提供对外服务,slave节点只提供备份服务,需要注意的是,并不是一个节点就是所有队列的master节点。谈论master和slave是针对队列来谈论的。
以Kafka为例:
每个topic有多个partion,每个partion分布于不同的节点,而且partion有副本存在于其他节点,每个partion有leader和follow,读写只存在于leader,follow会自动同步leader的数据,如果leader挂了,将重新选举一个leader。
2.消息被重复消费
rabbitMq场景:消费确认机制,如果设定了手动提交,消费者收到消息突然蹦了,导致ACK状态码未反馈至MQ
kafka场景:消费者会定期执行消费到的offset值提交给zookeeper用于给kafka节点从哪个位置发送消息为依据,但是由于极端原因如重启导致没有消费了消息但是没有提交,kafka任会按照上一次offset的位置发送消息给消费者,这就导致了重复消费
另外一种场景是:原本是想发送广播消息到多个消费者服务中,但是一个服务实例部署了多台,多台重复消费了。
解决方案:设计消费方法为幂等的。这个思路可以是在redis存储消费过后的一个标志,下次消费就先判断该标志存不存在,存在则不进行操作。或者基于数据库唯一键实现,报错就报错。
3.消息积压
场景:消费端故障导致消息积压
解决方案:
- 快速排查好消费端故障
- 改造原来的消费端,写到别的服务器的队列中,然后临时多开几个消费端按照原有逻辑去消费这些队列的数据
- 如果还设置了过期失效(一般情况下不会这么设置),部分数据丢了怎么办,手动写程序把丢掉的数据查出来再手动发到消息队列中去
4.消息丢失
以rabbitMQ为例,消息丢失可以分为:
(1)生产者丢消息
主要是因为,写消息的时候由于网络原因没有到rabbitmq服务器。
解决方案:
1.一是可以开启rabbitmq的事务模式
一次事务交互主要有以下环节:
客户端发送给服务器Tx.Select(开启事务模式)
服务器端返回Tx.Select-Ok(开启事务模式ok)
推送消息
客户端发送给事务提交Tx.Commit
服务器端返回Tx.Commit-Ok
以上就完成了事务的交互流程,如果其中任意一个环节出现问题,就会抛出IoException移除,这样用户就可以拦截异常进行事务回滚,或决定要不要重复消息。但是这样的缺点相比下面说的confirm机制,事务是同步的,吞吐量会低一点。
2.二是可以用confirm机制
流程是:
先把channel设置为confirm模式,发送消息,发完就不管了
生产者提供一个接口,用于实现成功/失败回调后的方法
接收失败的话可以直接再重发一次
(2)MQ丢消息
MQ丢消息的情况可能是:rabbitmq接收到消息,在消费者消费之前挂掉了。
解决方案(两步):
1.设置队列元数据持久化,设为durable;
2.生产者设置消息持久化,delivery_mode=2。
另外:
若设置了持久化且开启了confirm,将在持久化之后才回调生产者。
若持久化过程中宕机,还是会丢失数据,除非结合confirm机制。
(3)消费者丢消息
消费者打开了autoAck机制(消费者收到消息自动响应mq消费到了数据),如果正在消费时宕机了,就丢消息了。
解决方案:关闭ack改为手动ack(如果要在意最终数据一致性,最好是在数据库本地事务之后手动发送ack),如果mq没收到ack,mq将把消息发送给其他消费者。
以kafka为例:
(1) 生产者丢消息
如果设置了ack=all,一定不会丢,同时设置了retries=max,则会无限重试
(2) MQ丢消息
场景:有可能内存还没同步到日志文件或者没有同步到Follower但是自己down掉了。
参考解决方案:
topic设置repication.factor参数,必须大于1,要求每个Partition必须至少有2个副本。
kafka服务端设置min.insync.replicas大于1,要求一个leader至少需要感知到一个follower还跟自己保持联系。
生产者设置acks=all,要求每条数据必须是写入所有replica之后,才能认为是写成功了。
在producer端设置retries=max(很大的值),要求一旦写入失败,就无线重试。
(3)消费者丢消息
消费者自动提交了offset,但是处理业务中却异常宕机。取消offset,改为手动提交。
3.消息顺序变了
消息顺序有时候会影响业务,比如先修改后删除的两条消息,不按顺序来消费就会出错。
从rabbitMQ上来说,需要保证顺序的队列避免多个消费者同时消费(避免工作队列,只有一个消费者去消费)。
从kafka上来说,多个消费者同时消费的消息指定到了不同的key分发到了不同的partion导致不同消费者同时消费。因为kafka的一个partion只允许一个消费者,所以只需要把保证顺序的消息发送到同一个pation,让其被同一个消费者消费就行了。
另外也有可能是消费者自己并发的去消费信息造成的,针对这种情况,我们可以通过对消息的唯一标识进行hash算法分派到不同的内存队列,然后线程只取其中一个内存队列的消息进行消费来控制相关联的消息顺序不会乱。
4.一致性问题
一般使用消息队列只要配置对,使用消息确认机制,是能够保证一条消息的可靠投递的,问题主要在于如果一个事务发起方发送了多条消息,那么多条消息的最终一致性怎么解决,针对此rocketMq有成熟的解决方案,但其他消息队列没有,我们可以自己写一个中间的消息服务维护这种确认机制(上篇博文有)。