(四)消息类型 & 幂等性 & 重试机制
前面我们介绍了消息中间件的优势和选型,但是选择消息中间件时还需要考虑几个问题,支持的消息类型、如何保证消息不丢失、
消息幂等性的保证,下面我们逐个介绍:
1 常见消息类型
常见的消息类型包括无序消息、有序消息和延时消息三种。
1.1 无序消息
概念:无序消息即没有顺序的消息,具体的:producer 只管发送消息,consumer 只管接收消息,消息和消息之间的顺序并没有保证,可能先发送的消息先消费,也可能先发送的消息后消费。举个简单例子,producer 依次发送 order id 为 1、2、3 的消息到 broker,consumer 接到的消息顺序有可能是 1、2、3,也有可能是 2、1、3 等情况,这就是普通消息。
优势:因为不需要保证消息的顺序,所以消息可以大规模并发地发送和消费,吞吐量很高,适合大部分场景。
1.2 有序消息
概念:按照一定的先后顺序的消息类型,举个例子,producer 依次发送 order id 为 1、2、3 的消息到 broker,consumer 接到的消息顺序也就是 1、2、3 ,而不会出现普通消息那样的 2、1、3 等情况。但是RocketMQ只是保证的是消息发送有序,无法保证消费有序。
分类:有序消息分为全局有序消息和局部有序消息两种类型。
对于全局有序消息,将对应该类型的topic的queue数变为1,这样一来,只要 producer 按照 1、2、3、4 的顺序去发送消息,那么 consumer 自然也就按照 1、2、3、4 的顺序去消费,这就是全局有序消息。但由于一个topic只有一个queue,即使我们有多个producer实例和consumer实例也很难提高消息吞吐量,效率低下。
对于局部有序消息,实际上可看作吞吐量和有序之间的折中方案,以订单消息为例,订单消息可以再细分为订单创建、订单付款、订单完成等消息,这些消息都有相同的 orderId。对于某一个orderId只有顺序处理才符合业务逻辑。但不同orderId的消息是可以并行的,不会影响到业务。这时候就常见做法就是将orderId进行处理,将orderId相同的消息发送到topicB的同一个queue,不同orderId的消息发送到不同的queue中,从而实现局部有序。具体的,假设我们topicB有2个queue,那么我们可以对id取余,奇数的发往queue0,偶数的发往queue1,消费者按照queue消费时,就能保证queue0里面的消息有序消费,queue1里面的消息有序消费。由于一个topic可以有多个queue,假设queue数是n,理论上性能就是全局有序的n倍。
1.3 延时消息
概念:延时消息,简单来说就是当producer将消息发送到broker后,会延时一定时间后才投递给consumer进行消费。RcoketMQ的延时等级为:1s,5s,10s,30s,1m,2m,3m,4m,5m,6m,7m,8m,9m,10m,20m,30m,1h,2h。level=0,表示不延时。level=1,表示 1 级延时,对应延时 1s。level=2 表示 2 级延时,对应5s,以此类推。
场景:适用于消息生产和消费之间有时间窗口要求的场景。比如说我们网购时,下单之后是有一个支付时间,超过这个时间未支付,系统就应该自动关闭该笔订单。那么在订单创建的时候就会就需要发送一条延时消息(延时15分钟)后投递给consumer,consumer接收消息后再对订单的支付状态进行判断是否关闭订单。
实现:Broker将延时消息以指定topic(SCHEDULE_TOPIC_XXXX)将消息进行持久化,同时定时任务ScheduleMessageService通过不断读取该topic的queueId,判断延时时间达到后进行消息的还原处理,当消息被还原后就可以被消费者消费。
2 消息幂等性
正常情况下,消费者消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。RocketMQ返回一个CONSUME_SUCCESS成功标志,让消息队列知道自己已经消费过了。消息幂等性实际上是保证消息不被重复消费,但是消息的幂等性无法由消息侧来保证,应该由业务方结合具体的业务来保证,具体分析如下:
- 发送消息后服务端应答过程中发生网络闪断,发送端意识到发送失败再次推送消息;
- 订阅消息收到后应答过程中发生网络闪断,服务端意识到消费失败再次推送消息;
- RocketMQ的broker或客户端重启、扩容时会触发Rebalance,此时订阅端可能会收到重复消息
业务方保证消息幂等的建议:以业务的唯一标识Msg key作为幂等处理的依据,将收到消息的Msg key与所存储的Msg key集合做对比,如果不存在则接收消息并存储Msg Key,否则则忽略消息。
3 消息重试机制
消息重试机制的目的是为了避免消息丢失。要掌握重试机制,需要先了解重试队列和死信队列,如下:
- 重试队列:如果consumer端因各种异常导致本次消费失败,为防止该消息丢失而需要将其重新保存到broker中,保存这种因异常无法正常消费的消息队列称为重试队列,重试队列的名称有特定规则:%RETRY%+GroupID,需要注意的是重试队列是针对Group级别的而非topic级别的。但是考虑到消息重试有一定的时间间隔,重试次数越多投递延时越大(默认重试队列数为1个,最大重试次数为16次),因此,RocketMQ对重试消息的处理是先保存至Topic名为"SCHEDULETOPIXXX"的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至对应重试队列中投递。
- 死信队列:当超过重试次数后若消息仍然无法被消费,为了避免消息不会被无故的丢失,会将消息保存到死信队列中。其中,死信队列是针对Group级别的,命名规则为:"%DLQ%+GroupId"
以上阐述的是消费端相关的重试机制,实际上,RocketMQ支持生产端和消费端两类重试机制,具体的:
- 生产端:如果由于网络抖动等原因,生产端程向Broker发送消息时没有成功,即发送超时期间没有收到Broker的ACK,此时RocketMQ会自动进行重试。
- 消费端:消费者消费消息后,需要给Broker返回消费状态(SUCCESS/LATER)。消费端重试包括异常重试和超时重试两种方式,其中,异常重试指的是消费端逻辑出现异常导致返回LATER的状态,那么broker就会在一段时间后尝试重试,超时重试指的是consumer端处理时间太长,broker认为consumer消费超时,此时会发起超时重试。