消息重试
Rocketmq提供了消息重试机制,这是一些其他消息队列没有的功能。我们可以依靠这个优秀的机制,而不用在开发中增加更多的业务代码去实现
Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer 消费消息失败通常可以认为有以下几种情况
由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。
这种错误通常需要跳过这条消息,再消费其他消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过 10s 秒后再重试。由于依赖的下游应用服务不可用,例如 db 连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。
这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力。
消息重试可以分为生产者和消费者两端
生产者:消息重投重试(保证数据的高可靠性)
Producer 的 send 方法本身支持内部重试,重试逻辑如下
以上是官方3.2.6版本用户指南给出的方案
消费者:消息处理异常(broker端到consumer端各种问题,比如网络原因闪断,消费处理失败,ACK返回失败等等问题)
我们更多的关注点在于消费者这边的消息重试。消费处理失败的情况需要进行消息重试,如果是网络原因闪断或者ACK返回失败等原因,涉及到rocketmq前面说的集群模式,涉及到了消息去重。
如果有关注消费者中注册消息监听器MessageListenerConcurrently中重写的consumeMessage就会发现返回值是ConsumeConcurrentlyStatus(顺序消费的话,返回值不同)。就会发现该类有两个常量属性ConsumeConcurrentlyStatus.CONSUME_SUCCESS和ConsumeConcurrentlyStatus.RECONSUME_LATER
你可以try{}catch(){}消息消费的过程,如果业务上出现任何消费失败的情况下,catch到后,返回ConsumeConcurrentlyStatus.RECONSUME_LATER。consumer会将该ACK状态码返回给broker,broker便会稍后在进行消息发送。
消息重试是有指定时间了,默认第一次1s 第二次5s,以次类推给出的消息重试的时间间隔为:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
我们可以有两种方式得到消息已经重试多少次,从而对其进行业务判断,是否终止消息的重试
从在consumeMessage中获取的Message对象中调用getReconsumeTimes()方法,即msg.getReconsumeTimes()方法
MessageConst类有定义很多与消息相关的常量,比如消息原始id(PROPERTY_ORIGIN_MESSAGE_ID),比如消息重试的次数(PROPERTY_RECONSUME_TIME)
所以我们可以调用msg.getProperties().get()方法,传入常量键名,从众多的properties中获取咱们需要的属性
消息幂等,去重
broker不可避免会发送重复的消息给consumer。比如网络原因闪断,ACK返回失败等情况出现,将会导致消息重复。consumer必须保证处理的消息时唯一性
消息重复消费的原因
在于回馈机制。正常情况下,消费者在消费消息时候,消费完毕后,会发送一个ACK确认信息给消息队列(broker),消息队列(broker)就知道该消息被消费了,就会将该消息从消息队列中删除。
不同的消息队列发送的确认信息形式不同,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka实际上有个offset的概念。
造成重复消费的原因?,就是因为网络原因闪断,ACK返回失败等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。(因为消息重试等机制的原因,如果一个consumer断了,rocketmq有consumer集群,会将该消息重新发给其他consumer)
消息去重
去重原则:1.幂等性 2.业务去重
幂等性:(处理必须唯一) 无论这个业务请求被(consumer)执行多少次,我们的数据库的结果都是唯一的,不可变的。
去重策略:去重表机制,业务拼接去重策略(比如唯一流水号)
1.建立一个消息表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键(primary key)或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突。
高并发下去重:采用Redis去重(key天然支持原子性并要求不可重复),但是由于不在一个事务,要求有适当的补偿策略
2.利用redis事务,主键(我们必须把全量的操作数据都存放在redis里,然后定时去和数据库做数据同步)—-即消费处理后,该处理本来应该保存在数据库的,先保存在redis
3.利用redis和关系型数据库一起做去重机制
4.拿到这个消息做redis的set的操作.redis就是天然幂等性
5.准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
消息模式
RocketMQ不遵循JMS规范,可以理解为没有类似于ActiveMQ的createQueue和createTopic语法,也就是没有点对点和发布订阅模型。但是支持集群和广播两种消费模式
前面有讲过关于rocketmq的集群模式和广播模式,他们的设置都是在consumer端设置其messageModel属性
集群模式:设置消费端对象属性:MessageModel.CLUSTERING,这种方式可以达到类似于ActiveMQ水平扩展负责均衡消费消息的实现,但是不一样的是它是天然负载均衡的。该模式可以先启动生产端,再启动消费端,消费端仍然可以消费到生产端的消息,不过时间不一定.默认该模式
广播模式:设置消费端对象属性:MessageModel.BROADCASTING,这种模式就是相当于生产端发送数据到MQ,多个消费端都可以获得到数据。这个模式消费端必须先开启
GroupName,无论是生产端还是消费端,都必须指定一个GroupName,这个组名称,应用于维护应用系统级别上的。比如生产端一定同一个ProducerGroupName,应用系统会保证唯一性,这个组下的Producer通常发送一类消息,且发送逻辑一致。同理消费端也如此
Topic主题,每个主题代表一个逻辑上存储的概念。在MQ上,会有多个与之对应的Queue队列,这个是物理存储的概念
String group_name = "message_consumer";
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(group_name);
consumer.setNamesrvAddr(你的namesrvAddr);
consumer.subscribe("TopicTest","*");
consumer.setMessageModel(MessageModel.BROADCASTING);