1、需求分析
在 Elasticsearch实现服务搜索功能一文中,我们有这样一个场景:Canal将修改记录发送到MQ,同步程序监听MQ,收到增加、修改、删除数据的记录,请求ES创建、修改、删除索引
上述有一个关键点,首先数据增删改的信息是保证写入binlog的,Canal解析出增删改的信息后写入MQ,同步程序从MQ去读取消息,如果MQ中的消息丢失了数据将无法同步
2、如何保证MQ消息的可靠性?
2.1、保证生产消息的可靠性
RabbitMQ
提供生产者确认机制保证生产消息的可靠性

1、首先发送消息的方法如果执行失败会进行重试,重试次数耗尽记录失败消息
重试===》@Retryable注解可实现方法执行失败进行重试 说明如下:
value:抛出指定异常才会重试
include:和value一样,默认为空,当exclude也为空时,默认所有异常
exclude:指定不处理的异常
maxAttempts:最大重试次数,默认3次
backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为3000L;表示第一次失败后等待3秒后重试,multiplier(指定延迟倍数)默认为0,如果把multiplier设置为1.5表示每次等待重试时间是上一次的1.5倍,则第一次重试为3秒,第二次为4.5秒,第三次为6.75秒。
java
@Retryable(value = MqException.class, maxAttempts = 3, backoff = @Backoff(value = 3000, multiplier = 1.5), recover = "saveFailMag")
记录失败消息===》 Recover: 设置回调方法名,当重试耗尽时,通过recover属性设置回调的方法名。通过@Recover注解定义重试失败后的处理方法,在Recover方法中记录失败消息到数据库。
java
/**
* @param mqException mq异常消息
* @param exchange 交换机
* @param routingKey 路由key
* @param msg mq消息
* @param delay 延迟消息
* @param msgId 消息id
*/
@Recover
public void saveFailMag(MqException mqException, String exchange, String routingKey, Object msg, Integer delay, String msgId) {
//发送消息失败,需要将消息持久化到数据库,通过任务调度的方式处理失败的消息
failMsgDao.save(mqException.getMqId(), exchange, routingKey, JsonUtils.toJsonStr(msg), delay, DateUtils.getCurrentTime() + 10, ExceptionUtil.getMessage(mqException));
}
2、通过MQ的提供的生产者确认机制保证生产消息的可靠性
使用生产者确认机制 需要给每个消息指定一个唯一ID,生产者确认机制通过异步回调的方式进行,包括ConfirmCallback
和Return
回调。
ConfirmCallback: 消息发送到Broker会有一个结果返回给发送者表示消息是否处理成功:
1)消息成功投递到 交换机 ,返回 ack
2)消息未投递到 交换机 ,返回nack
Return 回调 : 如果消息发送到交换机成功了但是并没有到达队列,此时会调用ReturnCallback回调方法,在回调方法中我们可以收到失败的消息存入失败消息表以便进行补偿。
要使用Return回调需要开启设置:
首先在shared-rabbitmq.yaml中配置rabbitMQ参数,如下:
yml
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
说明:
publish-confirm-type:开启publisher-confirm,这里支持两种类型:
simple:同步等待confirm结果,直到超时
correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
2.2、保证消费消息可靠性
首先设置消息持久化,保证消息发送到MQ消息不丢失。具体需要设置交换机和队列支持持久化
,发送消息设置deliveryMode=2。
RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK
回执,表明自己已经处理完成消息,RabbitMQ收到ACK后删除消息。

消费消息失败重试3次,仍失败则将消费失败的消息入库。
通过任务调度扫描失败消息表重新发送,达到一定的次数还未成功则由人工处理。
RabbitMQ提供三个确认模式:
•manual:手动ack,需要在业务代码结束后,调用api发送ack。
•auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
•none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
由此可知:
- none模式下,消息投递是不可靠的,可能丢失
- auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
- manual:自己根据业务情况,判断什么时候该ack
一般,我们都是使用默认的
auto
即可。当消费消息失败会重试,重试3次如果还失败会将消息投递到失败消息队列,由定时任务程序定时读取队列的消息,达到一定的次数还未成功则由人工处理。
yml
spring:
rabbitmq:
....
listener:
simple:
acknowledge-mode: auto #,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 10 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
3、如何防止重复消费?
消费者在消费消息时难免出现重复消费的情况,比如:消费者没有向MQ返回ack导致重复消费,所以消费者需要保证消费消息
幂等性
。

幂等性是指无论执行多少次其结果都是一致的
举例: 收到消息需要向数据新增一条记录,如果重复消费则会出现重复添加记录的问题。
下边根据场景分析解决方案:
1、查询操作
本身具有幂等性。
2、添加操作
如果主键是自增则可能重复添加记录。
保证幂等性可以设置数据库的唯一约束,比如:添加学生信息,将学号字段设置为唯一索引,即使重复添加相同的学生同一个学号只会添加一条记录。
3、更新操作
如果是更新一个固定的值,比如: update users set status =1 where id=?,本身具有幂等性。
如果只允许更新成功一次则可以使用token机制,发送消息前生成一个token写入redis,收到消息后解析出token从redis查询token如果成功则说明没有消费,此时更新成功将token从redis删除,当重复消费相同 的消息时由于token已经从redis删除不会再执行更新操作。
4、删除操作
与更新操作类似,如果是删除某个具体的记录,比如:delete from users where id=?,本身具有幂等性。
如果只允许删除成功一次可以采用更新操作相同的方法。
通过以上分析进行总结: 1、使用数据库的唯一约束去控制 。 2、使用token机制 ,如下: 消息具有唯一的ID, 发送消息时将消息ID写入Redis, 消费时根据消息ID查询Redis判断是否已经消费,如果已经消费则不再消费。

4、小结
问题:可以百分百保证MQ的消息可靠性吗?
答案:不能百分百做的可靠
保证消息可靠性分两个方面:保证生产消息可靠性和保证消费消息可靠性。
保证生产消息可靠性:
生产消息可靠性是通过判断MQ是否发送ack回执 ,如果发nack表示发送消息失败,此时会进行重发或记录到失败消息表,通过定时任务进行补偿发送。如果Java程序并没有收到回执(如jvm进程异常结束了,或断电等因素),此时将无法保证生产消息的可靠性。
保证消费消息可靠性:
保证消费消息可靠性方案首先保证发送消息设置为持久化,其次通过MQ的消费确认机制保证消费者消费成功消息后再将消息删除。
虽然设置了消息持久化,消息进入MQ首先是在缓存存在,MQ会根据一定的规则进行刷盘,比如:每隔几毫秒进行刷盘,如果在消息还没有保存到磁盘时MQ进程终止,此时将会丢失消息。虽然可以使用镜像队列(用于在 RabbitMQ 集群中复制队列的消息,这样做的目的是提高队列的可用性和容错性,以防止在单个节点故障时导致消息的丢失。)但也不能百分百保证消息不丢失。
虽然我们加了很多保证可靠性的机制,这样也只是去提高消息的可靠性,不能百分百做的可靠,所以使用MQ的场景要考虑这种问题的存在,做好补偿处理任务。