前言
RocketMQ是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式的特点。它是一个采用Java语言开发的分布式的消息系统,由阿里巴巴团队开发,在2016年底贡献给Apache,成为了Apache的一个顶级项目。 在阿里内部,RocketMQ很好地服务了集团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级消息通过RocketMQ流转。架构图:
1、消息丢失
1.1、Producer发送消息阶段
1.1.1、提供SYNC的发送消息方式,等待broker处理结果
RocketMQ提供了3种发送消息方式,分别是:
- 同步发送:Producer 向 broker 发送消息,阻塞当前线程等待 broker 响应 发送结果。
- 异步发送:Producer 首先构建一个向 broker 发送消息的任务,把该任务提交给线程池,等执行完该任务时,回调用户自定义的回调函数,执行处理结果。
- Oneway发送:Oneway 方式只负责发送请求,不等待应答,Producer只负责把请求发出去,而不处理响应结果。
我们在调用producer.send方法时,不指定回调方法,则默认采用同步发送消息的方式,这也是丢失几率最小的一种发送方式。
1.1.2、发送消息如果失败或者超时,则重新发送
代码中可以通过for循环,当发送消息发生异常的时候重新循环发送。默认重试3次,重试次数可以通过producer指定。
1.1.3、broker提供多master模式,即使某台broker宕机了,保证消息可以投递到另外一台正常的broker上
如果broker只有一个节点,则broker宕机了,即使producer有重试机制,也没用,因此利用多主模式,当某台broker宕机了,换一台broker进行投递。
1.1.4、总结
producer消息发送方式虽然有3种,但为了减小丢失消息的可能性尽量采用同步的发送方式,同步等待发送结果,利用同步发送+重试机制+多个master节点,尽可能减小消息丢失的可能性。
1.2、Broker处理消息阶段
1.2.1、设置同步刷盘的策略
public enum FlushDiskType { SYNC_FLUSH, //同步刷盘 ASYNC_FLUSH//异步刷盘(默认) }
当消息投递到broker之后,会先存到page cache,然后根据broker设置的刷盘策略是否立即刷盘,也就是如果刷盘策略为异步,broker并不会等待消息落盘就会返回producer成功,也就是说当broker所在的服务器突然宕机,则会丢失部分页的消息。
1.2.2、提供主从模式,同时主从支持同步双写
即使broker设置了同步刷盘,如果主broker磁盘损坏,也是会导致消息丢失。 因此可以给broker指定slave,同时设置master为SYNC_MASTER,然后将slave设置为同步刷盘策略。
此模式下,producer每发送一条消息,都会等消息投递到master和slave都落盘成功了,broker才会当作消息投递成功,保证休息不丢失。
1.2.3、总结
在broker端,消息丢失的可能性主要在于刷盘策略和同步机制。RocketMQ默认broker的刷盘策略为异步刷盘,如果有主从,同步策略也默认的是异步同步,这样可以提高broker处理消息的效率,但是会有丢失的可能性。因此可以通过同步刷盘策略+同步slave策略+主从的方式解决丢失消息的可能。
1.3、Consumer消费消息阶段
1.3.1、consumer默认提供的是At least Once机制
从producer投递消息到broker,即使前面这些过程保证了消息正常持久化,但如果consumer消费消息没有消费到也不能理解为消息绝对的可靠。因此RockerMQ默认提供了At least Once机制(Consumer先pull 消息到本地,消费完成后,才向服务器返回ack)保证消息可靠消费。通常消费消息的ack机制一般分为两种思路:
- 先提交后消费,可以解决重复消费的问题但是会丢失消息;
- 先消费,消费成功后再提交。即Rocketmq默认实现的思路,由各自consumer业务方保证幂等来解决重复消费问题。
2、消息幂等
2.1、场景
-
发送时消息重复
当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同但Message ID不同的消息。或者发送消息者主动(bug导致)消息体一样Message ID不同的消息。
-
投递时消息重复
消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RocketMQ版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。
-
负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启)
当消息队列RocketMQ版的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到少量重复消息。
2.2、处理办法
处理方法可以从消息生产者和消息消费者入手,因为不同的消息Message ID可能对应相同的消息内容。可能出现重现重复情况,使用Message ID不建议。RocketMQ 提供了可以设置消息的Key, key可以由用户自定义,上面的案例code就是唯一值,那么code就能作为处理消息幂等的依据。
消息生产者处理:
Message message = new Message();
message.setKey("4500000237");
SendResult sendResult = producer.send(message);
复制代码
Tips: Message也不用设置Key,直接通过消息体中的唯一值字段处理
消费者接收到消息时可以根据消息key(或者消息体中的唯一值)。
消费者处理:
consumer.subscribe("ons_test", "*", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
String key = message.getKey()
// 根据业务唯一标识的Key做幂等处理。
}
});
//或者
consumer.subscribe("ons_test", "*", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
String body = message.getBody();
// 解析出body的唯一值做幂等处理
}
});
复制代码
接下来就是消费者如何根据唯一值做幂等处理。
2.3、消费者消息幂等处理
消息的幂等处理需要看消费者服务的部署情况,这里需要区分是单机部署还是集群两种情况。
单服务部署处理方式:
-
数据库对唯一值的入库字段设唯一索引,如果存在相同的唯一值存在插入数据就会报错。只需要处理相对应的错误即可。
-
通过锁处理,对插入数据的步骤加锁(本地锁或者数据库锁)
SELECT * FROM auth_allocation WHERE deleted = 1 AND biz_code = 'xxxx' for update 复制代码
集群消费服务部署:
- 使用数据库的行锁处理
- 利用分布式锁处理不同服务间的并发。
- 数据库对唯一值的入库字段设唯一索引。
对应上述案例,如果不能设置数据库唯一索引,只能通过分布式锁或者数据库的行锁来处理消息的幂等。
2.4、总结
- 消息消费失败做好回滚处理。
- 一些无法做到幂等的操作,需要发送警告给相关人员进行手动处理。
3、顺序消息
RocketMQ在主题上是无序的、只有在队列层面才是保证有序的。有序分两个概念:
- 普通顺序是指消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。普通顺序消息在Broker重启情况下不会保证消息顺序性(短暂时间) 。
- 严格顺序是指消费者收到的所有消息均是有顺序的。严格顺序消息即使在异常情况下也会保证消息的顺序性。看起来虽好,实现它可会付出巨大的代价,即Broker集群中只要有一台机器不可用,则整个集群都不可用。
所以推荐使用普通顺序模式,业务一般可以容忍短暂的乱序,这种模式下,在Producer生产消息的时候会进行轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息。如果需要将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用Hash取模法来保证。
4、消息事务
分布式事务实现有 2PC、TCC 和事务消息(half 半消息机制)。每一种实现都有其特定的使用场景,但是也有各自的问题,都不是完美的解决方案。在RocketMQ中使用的是事务消息加上事务反查机制来解决分布式事务问题的。
5、消息堆积
根源:生产者生产太快或者消费者消费太慢。
当流量到峰值的时候是因为生产者生产太快,我们可以使用一些限流降级的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查是否是消费者出现了大量的消费错误,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。
当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过同时你还需要增加每个主题的队列数量,为会被一个消费者消费,所以两个都要增加。
6、回溯消费
回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,在RocketMQ中,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。
7、刷盘机制
7.1、同步刷盘和异步刷盘
- 同步刷盘中需要等待一个刷盘成功的ACK,同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般地适用于金融等特定业务场景。
- 异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
一般地,异步刷盘只有在Broker意外宕机的时候会丢失部分数据,你可以设置Broker的参数FlushDiskType来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。
7.2、同步复制和异步复制
上面的同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的Borker主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。
- 同步复制: 也叫 “同步双写”,也就是说,只有消息同步双写到主从节点上时才返回写入成功。
- 异步复制: 消息写入主节点之后就直接返回写入成功。