前三步不一定能保障消息能够100%投递成功。因此要加上第四步
BAT/TMD 互联网大厂的解决方案:
消息落库,对消息状态进行打标
在发送消息的时候,需要将消息持久化到数据库中,并给这个消息设置一个状态(未发送、发送中、到达)。当消息状态发生了变化,需要对消息做一个变更。针对没有到达的消息做一个轮训操作,重新发送。对轮训次数也需要做一个限制3-5次。确保消息能够成功的发送.
消息的延迟投递,做二次确认,回调检查
具体采用哪种方案,还需要根据业务与消息的并发量而定。
比如:如下发一条订单消息。
step1:存储订单消息(创建订单),业务数据入库,消息也入库。缺点:需要持久化两次。(status:0)
step2:在step1成功的前提下,发送消息
step3:Broker收到消息后,confirm给我们的生产端。Confirm Listener异步监听Broker回送的消息。
step4:抓取出指定的消息,更新(status=1),表示消息已经投递成功。
step5:分布式定时任务获取消息状态,如果等于0则抓取数据出来。
step6:重新发送消息
step7:重试限制设置3次。如果消息重试了3次还是失败,那么(status=2),认为这个消息就是失败的。
查询这些消息为什么失败,可能需要人工去查询。
假设step2执行成功,step3由于网络闪断。那么confirm将永远收不到消息,那么我们需要设定一个规则:
例如:在消息入库的时候,设置一个临界值 timeout=5min,当超过5min之后,就将这条数据抓取出来。
或者写一个定时任务每隔5分钟就将status=0的消息抓取出来。可能存在小问题:消息发送出去,定时任务又正好刚执行,Confirm还未收到,定时任务就会执行,会导致消息执行两次。
更精细化操作:消息超时容忍限制。confirm在2-3分钟内未收到消息,则重新发送。
注意:面对小规模的应用可以采用加事务的方式,保证事务的一致性。但在大厂中面对高并发,并没有加事务,事务的性能拼接非常严重,而是做补偿。
保障MQ我们思考如果第一种可靠性投递,在高并发的场景下是否合适?
第一种方案对数据有两次入库,一次业务数据入库,一次消息入库。这样对数据的入库是一个瓶颈。
其实我们只需要对业务进行入库。
消息的延迟投递,做二次确认,回调检查
这种方式并不一定能保证100%成功,但是也能保证99.99%的消息成功。如果遇到特别极端的情况,那么就只能需要人工去补偿,或者定时任务去做。
第二种方式主要是为了减少对数据库的操作。
step1:业务消息入库成功后,第一次消息发送。
step2:同样在消息入库成功后,发送第二次消息,这两条消息是同时发送的。第二条消息是延迟检查,可以设置2min、5min 延迟发送。
step3:消费端监听指定队列。
step4:消费端处理完消息后,内部生成新的消息send confirm。投递到MQ Broker。
step5: Callback Service 回调服务监听MQ Broker,如果收到Downstream service发送的消息,则可以确定消息发送成功,执行消息存储到MSG DB。
step6:Check Detail检查监听step2延迟投递的消息。此时两个监听的队列不是同一个,5分钟后,Callback service收到消息,检查MSG DB。如果发现之前的消息已经投递成功,则不需要做其他事情。如果检查发现失败,则Callback 进行补偿,主动发送RPC 通信。通知上游生产端重新发送消息。
这样做的目的:少做了一次DB存储。关注点并不是百分百的投递成功,而是性能。
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中,即f(f(x)) = f(x)。简单的来说就是一个操作多次执行产生的结果与一次执行产生的结果一致。
利用加版本号Version的方式来保证幂等性。
在海量订单产生的业务高峰期,如何避免消息的重复消费问题?
在高并发的情况下,会有大量的消息到达MQ,消费端需要监听大量的消息。这样的情况下,难免会出现消息的重复投递,网络闪断等等。如果不去做幂等,则会出现消息的重复消费。
-消费端实现幂等性,就意味着,我们的消息永远不会被消费多次,即使我们收到了多条一样的消息,也只会执行一次。
看下互联网大厂主流的幂等性操作:
-唯一ID+指纹吗机制,利用数据库主键去重。
-利用Redis的原子性实现
-其他的技术实现幂等性
最简单使用Redis的自增。
理解Confirm 消息确认机制:
spring:
application:
name: zoo-plus-rabbitmq
rabbitmq:
virtual-host: /
host: localhost
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated #必须配置这个才会确认回调
生产者:
/**
* 测试消息确认回调(必须在yml配置publisher-confirm-type: correlated)
*/
@GetMapping("sendConfirmCallback")
public Resp sendConfirmCallback() {
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
System.out.println("ack:" + ack);
if (!ack) {
//可以进行日志记录、异常处理、补偿处理等
System.out.println("异常处理");
}else{
//更新数据库,可靠性投递机制
}
});
amqpTemplate.convertAndSend("test-queue", "测试ack确认模式");
return Resp.success("ok", null);
}
在基础API中有一个关键的配置项:
配置
spring:
application:
name: zoo-plus-rabbitmq
rabbitmq:
virtual-host: /
host: localhost
port: 5672
username: guest
password: guest
publisher-returns: true #支持发布返回
代码实现
/**
* 启动消息失败返回,比如路由不到队列时触发回调
* 测试发布回调(必须在yml配置publisher-returns: true)
*/
@GetMapping("sendReturnCallback")
public Resp sendReturnCallback() {
RabbitTemplate.ReturnCallback returnCallback = (message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("========returnCallback=============");
};
rabbitTemplate.setReturnCallback(returnCallback);
amqpTemplate.convertAndSend("test-", "测试发布回调模式");
return Resp.success("ok", null);
}
源码地址:https://gitee.com/zoo-plus/springboot-learn/tree/2.x/springboot-middleware/rabbitmq