asp access网站架设教程桂林企业建站
web/
2025/10/9 9:16:17/
文章来源:
asp access网站架设教程,桂林企业建站,怎样卸载wordpress,计算机平面设计是干什么的在之前的练习作业中#xff0c;我们改造了余额支付功能#xff0c;在支付成功后利用RabbitMQ通知交易服务#xff0c;更新业务订单状态为已支付。
但是大家思考一下#xff0c;如果这里MQ通知失败#xff0c;支付服务中支付流水显示支付成功#xff0c;而交易服务中的订… 在之前的练习作业中我们改造了余额支付功能在支付成功后利用RabbitMQ通知交易服务更新业务订单状态为已支付。
但是大家思考一下如果这里MQ通知失败支付服务中支付流水显示支付成功而交易服务中的订单状态却显示未支付数据出现了不一致。
此时前端发送请求查询支付状态时肯定是查询交易服务状态会发现业务订单未支付而用户自己知道已经支付成功这就导致用户体验不一致。 因此这里我们必须尽可能确保MQ消息的可靠性即消息应该至少被消费者处理1次
那么问题来了 我们该如何确保MQ消息的可靠性 如果真的发送失败有没有其它的兜底方案 1.发送者的可靠性
首先我们一起分析一下消息丢失的可能性有哪些。
消息从发送者发送消息到消费者处理消息需要经过的流程是这样的 消息从生产者到消费者的每一步都可能导致消息丢失 发送消息时丢失 生产者发送消息时连接MQ失败 生产者发送消息到达MQ后未找到Exchange 生产者发送消息到达MQ的Exchange后未找到合适的Queue 消息到达MQ后处理消息的进程发生异常 MQ导致消息丢失 消息到达MQ保存到队列后尚未消费就突然宕机 消费者处理消息时 消息接收后尚未处理突然宕机 消息接收后处理过程中抛出异常 综上我们要解决消息丢失问题保证MQ的可靠性就必须从3个方面入手 确保生产者一定把消息发送到MQ 确保MQ不会将消息弄丢 确保消费者一定要处理消息 1.1.生产者重试机制
首先第一种情况就是生产者发送消息时出现了网络故障导致与MQ的连接中断。 为了解决这个问题SpringAMQP提供的消息发送时的重试机制。即当RabbitTemplate与MQ连接超时后多次重试。 修改publisher模块的application.yaml文件添加下面的内容
spring:rabbitmq:connection-timeout: 1s # 设置MQ的连接超时时间template:retry:enabled: true # 开启超时重试机制initial-interval: 1000ms # 失败后的初始等待时间multiplier: 1 # 失败后下次的等待时长倍数下次等待时长 initial-interval * multipliermax-attempts: 3 # 最大重试次数
我们利用命令停掉RabbitMQ服务
docker stop mq
然后测试发送一条消息会发现会每隔1秒重试1次总共重试了3次。消息发送的超时重试机制配置成功了 注超时时间设置的1s也就是说消息发送以后先等1s才算是超时超时了以后下次重试也要1s所以日志间隔2s。
注意当网络不稳定的时候利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试也就是说多次重试等待的过程中当前线程是被阻塞的。通俗来说在等待期间代码卡在那里了不会向下运行了。 如果对于业务性能有要求建议禁用重试机制。如果一定要使用请合理配置等待时长和重试次数当然也可以考虑使用异步线程来执行发送消息的代码。 1.2.生产者确认机制
相比于生产者重试机制生产者确认机制侧重的是消息发送时失败了怎么办。
一般情况下只要生产者与MQ之间的网路连接顺畅基本不会出现发送消息丢失的情况因此大多数情况下我们无需考虑这种问题。
不过在少数情况下也会出现消息发送到MQ之后丢失的现象比如 MQ内部处理消息的进程发生了异常 生产者发送消息到达MQ后未找到Exchange 生产者发送消息到达MQ的Exchange后未找到合适的Queue因此无法路由 针对上述情况RabbitMQ提供了生产者消息确认机制包括Publisher Confirm和Publisher Return两种。在开启确认机制的情况下当生产者发送消息给MQ后MQ会根据消息处理的情况返回不同的回执。
具体如图所示 总结如下 当消息投递到MQ但是路由失败时通过Publisher Return返回异常信息同时返回ack的确认信息代表投递成功。这种情况一般是RoutingKey填写失败跟我MQ发送没有关系 临时消息投递到了MQ并且入队成功返回ACK告知投递成功 持久消息投递到了MQ并且入队完成持久化保存到磁盘后才会返回ACK 告知投递成功 其它情况都会返回NACK告知投递失败
比如说消息投递到交换机结果还没来得及持久化到磁盘结果磁盘因为某种故障 (比如说磁盘满了)MQ内部出现异常 (比如说内存爆满)导致消息丢失。这些情况都会返回NACK。 其中ack和nack属于Publisher Confirm机制ack是投递成功nack是投递失败。而return则属于Publisher Return机制。
默认两种机制都是关闭状态需要通过配置文件来开启。 1.3.实现生产者确认 在我们上面分析的生产者确认机制里面生产者发消息MQ给我们回值我们应该去接收这个值。这里就有两种方法1.同步等待发了消息就开始等待等着MQ给我回值。2.采用异步回调的方式生产者发了消息就干别的事了。当MQ回值来了以后我再去处理就行了。
当然这种异步的方式会好一点接下来我们就采用这种方式。
1.3.1.开启生产者确认
在publisher模块的application.yaml中添加配置
spring:rabbitmq:publisher-confirm-type: correlated # 开启publisher confirm机制并设置confirm类型publisher-returns: true # 开启publisher return机制专门用来返回路由失败消息的
这里publisher-confirm-type有三种模式可选 none关闭confirm机制 simple同步阻塞等待MQ的回执 correlatedMQ异步回调返回回执 一般我们推荐使用correlated异步回调机制。 1.3.2.定义ReturnCallback (回调函数)
每个RabbitTemplate只能配置一个ReturnCallback因此我们可以在配置类中统一设置。我们在publisher模块定义一个配置类 内容如下
package com.itheima.publisher.config;import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;Slf4j
AllArgsConstructor
Configuration
public class MqConfig {private final RabbitTemplate rabbitTemplate;PostConstructpublic void init(){rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {Overridepublic void returnedMessage(ReturnedMessage returned) {log.error(触发return callback,);log.debug(exchange: {}, returned.getExchange());log.debug(routingKey: {}, returned.getRoutingKey());log.debug(message: {}, returned.getMessage());log.debug(replyCode: {}, returned.getReplyCode());log.debug(replyText: {}, returned.getReplyText());}});}
}
1.3.3.定义ConfirmCallback
由于每个消息发送时的处理逻辑不一定相同因此ConfirmCallback需要在每次发消息时定义。具体来说是在调用RabbitTemplate中的convertAndSend方法时多传递一个参数 这里的CorrelationData中包含两个核心的东西 id消息的唯一标示MQ对不同的消息的回执以此做判断避免混淆 SettableListenableFuture回执结果的Future对象 将来MQ的回执就会通过这个Future来返回我们可以提前给CorrelationData中的Future添加回调函数来处理消息回执 我们新建一个测试向系统自带的交换机发送消息并且添加ConfirmCallback
Test
void testPublisherConfirm() {// 1.创建CorrelationDataCorrelationData cd new CorrelationData(UUID.randomUUID().toString());// 2.给Future添加ConfirmCallbackcd.getFuture().addCallback(new ListenableFutureCallbackCorrelationData.Confirm() {Overridepublic void onFailure(Throwable ex) {// 2.1.Future发生异常时的处理逻辑基本不会触发// 回调出现问题这是spring内部出现问题才会触发和mq无关log.error(send message fail, ex);}Overridepublic void onSuccess(CorrelationData.Confirm result) {//指mq回调成功// 2.2.Future接收到回执的处理逻辑参数中的result就是回执内容if(result.isAck()){ // result.isAck()boolean类型true代表ack回执false 代表 nack回执log.debug(发送消息成功收到 ack!);}else{ // result.getReason()String类型返回nack时的异常描述log.error(发送消息失败收到 nack, reason : {}, result.getReason());}}});// 3.发送消息rabbitTemplate.convertAndSend(hmall.direct, q, hello, cd);
}
CollelationData对象这个对象里面有一个唯一IDUUID当前消息的一个标识。每次发消息都有一个CollelationData对象因为每个消息都有自己的消息id。将来消息到MQ以后MQ也能区分每个消息谁是谁。将来做回调的时候每个消息的回调函数可能不同。
注意这个onFailure和onSuccess方法是指回调有没有成功。不是指消息执行有没有成功。而onSuccess里根据ack还是nack才能知道消息有没有发送成功
执行结果如下 可以看到由于传递的RoutingKey是错误的路由失败后触发了return callback同时也收到了ack。
当我们修改为正确的RoutingKey以后就不会触发return callback了只收到ack。
而如果连交换机都是错误的则只会收到nack。
注意
开启生产者确认比较消耗MQ性能一般不建议开启。而且大家思考一下触发确认的几种情况 路由失败一般是因为RoutingKey错误导致往往是编程导致 交换机名称错误同样是编程错误导致 MQ内部故障这种需要处理但概率往往较低。因此只有对消息可靠性要求非常高的业务才需要开启而且仅仅需要开启ConfirmCallback处理nack就可以了。
总结RabbitMQ如何保证生产者的可靠性
1.配置生产者重连机制。(如当MQ网络出现波动的时候它会重试、重新连接)
2.配置生产者确认机制当我们开启了生产者确认机制以后生产者发消息到MQMQ就会给生产者一个回值ACK/NACK。告诉生产者消息是否发送成功如果发送失败就可以重发。
注以上手段都会增加系统开销因此大部分场景下不建议开启。除非是对消息可靠性较高的业务 2.MQ的可靠性
消息到达MQ以后如果MQ不能及时保存也会导致消息丢失所以MQ的可靠性也非常重要。
2.1.数据持久化
为了提升性能默认情况下MQ的数据都是在内存存储的临时数据重启后就会消失。为了保证数据的可靠性必须配置数据持久化包括 交换机持久化 队列持久化 消息持久化
我们以控制台界面为例来说明。
2.1.1.交换机持久化
在控制台的Exchanges页面添加交换机时可以配置交换机的Durability参数 设置为Durable就是持久化模式Transient就是临时模式。 2.1.2.队列持久化
在控制台的Queues页面添加队列时同样可以配置队列的Durability参数 除了持久化以外你可以看到队列还有很多其它参数有一些我们会在后期学习。 2.1.3.消息持久化
在控制台发送消息的时候可以添加很多参数而消息的持久化是要配置一个properties 说明在开启持久化机制以后如果同时还开启了生产者确认那么MQ会在消息持久化以后才发送ACK回执进一步确保消息的可靠性。
不过出于性能考虑为了减少IO次数发送到MQ的消息并不是逐条持久化到数据库的而是每隔一段时间批量持久化。一般间隔在100毫秒左右这就会导致ACK有一定的延迟因此建议生产者确认全部采用异步方式。
如何保证MQ的可靠性
1.通过配置将交换机、队列这两者Spring中默认持久化、发送的消息都持久化。这样队列中的消息会持久化到磁盘MQ重启消息依然存在。
2.LazyQueue作为默认的队列模式也会将消息都持久化。
3.开启持久化和生产者确认时RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执。
2.2.LazyQueue
在默认情况下RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。但在某些特殊情况下这会导致消息积压比如 消费者宕机或出现网络故障 消息发送量激增超过了消费者处理速度 消费者处理业务发生阻塞 一旦出现消息堆积问题RabbitMQ的内存占用就会越来越高直到触发内存预警上限。此时RabbitMQ会将内存消息刷到磁盘上这个行为成为PageOut. PageOut会耗费一段时间并且会阻塞队列进程。因此在这个过程中RabbitMQ不会再处理新的消息生产者的所有请求都会被阻塞。 为了解决这个问题从RabbitMQ的3.6.0版本开始就增加了Lazy Queues的模式也就是惰性队列。惰性队列的特征如下 接收到消息后直接存入磁盘而非内存 消费者要消费消息时才会从磁盘中读取并加载到内存也就是懒加载 支持数百万条的消息存储 而在3.12版本之后LazyQueue已经成为所有队列的默认格式。因此官方推荐升级MQ为3.12版本或者所有队列都设置为LazyQueue模式。 2.2.1.控制台配置Lazy模式
在添加队列的时候添加x-queue-modlazy参数即可设置队列为Lazy模式 2.2.2.代码配置Lazy模式
在利用SpringAMQP声明队列的时候添加x-queue-modlazy参数也可设置队列为Lazy模式
Bean
public Queue lazyQueue(){return QueueBuilder.durable(lazy.queue).lazy() // 开启Lazy模式.build();
}
这里是通过QueueBuilder的lazy()函数配置Lazy模式底层源码如下 当然我们也可以基于注解来声明队列并设置为Lazy模式
RabbitListener(queuesToDeclare Queue(name lazy.queue,durable true,arguments Argument(name x-queue-mode, value lazy)
))
public void listenLazyQueue(String msg){log.info(接收到 lazy.queue的消息{}, msg);
} 2.2.3.更新已有队列为lazy模式
对于已经存在的队列也可以配置为lazy模式但是要通过设置policy实现。
可以基于命令行设置policy
rabbitmqctl set_policy Lazy ^lazy-queue$ {queue-mode:lazy} --apply-to queues
命令解读 rabbitmqctl RabbitMQ的命令行工具 set_policy 添加一个策略 Lazy 策略名称可以自定义 ^lazy-queue$ 用正则表达式匹配队列的名字 {queue-mode:lazy} 设置队列模式为lazy模式 --apply-to queues策略的作用对象是所有的队列 当然也可以在控制台配置policy进入在控制台的Admin页面点击Policies即可添加配置 3.消费者的可靠性 3.1.消费者确认机制 为了确认消费者是否成功处理消息RabbitMQ提供了消费者确认机制Consumer Acknowledgement。即当消费者处理消息结束后应该向RabbitMQ发送一个回执告知RabbitMQ自己消息处理状态。回执有三种可选值 ack成功处理消息RabbitMQ从队列中删除该消息 nack消息处理失败RabbitMQ需要再次投递消息 reject消息处理失败并拒绝该消息RabbitMQ从队列中删除该消息 一般reject方式用的较少比如说消息格式有问题导致后端代码出错那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过try catch机制捕获消息处理成功时返回ack处理失败时返回nack. spring实现消息确认的逻辑 我们编写消息监听的业务处理逻辑这个逻辑最终由spring帮我们调用。底层使用动态代理代理消息监听器当监听到消息的时候动态代理对象就会去调用消息处理的逻辑。如果在消息处理的过程中是成功的spring就会帮我们返回ack。如果抛出的是普通运行异常就会返回nack如果是请求参数异常就会返回reject。 由于消息回执的处理代码比较统一因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式有三种模式 none不处理。即消息投递给消费者后立刻ack消息会立刻从MQ删除。非常不安全不建议使用 manual手动模式。需要自己在业务代码中调用api发送ack或reject存在业务入侵但更灵活 auto自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强当业务正常执行时则自动返回ack. 当业务出现异常时根据异常判断返回不同结果 如果是业务异常会自动返回nack 如果是消息处理或校验异常自动返回reject; 3.2.失败重试机制 消费者如何保证消息一定被消费
1.开启消费者确认机制为auto由spring确认消息处理成功后返回ack异常返回nack或reject。
2.开启消费者失败重试机制并设置MessageRecoverer(用于配置重试机制)多次重试失败后将消息投递到异常交换机。 3.3业务幂等性 目前为止经过以上一系列确保可靠性的方案我们已经能够确保消息至少被消费一次注意是至少被消费一次。但可能由于比如网络波动有可能消费者已经消费成功了但网络原因没有返回ack发送者以为消息超时了又重新发送消息。最终出现一个消息被消费多次的情况。
当出现重复消费时我们的业务需要保证幂等性
何为幂等性
幂等是一个数学概念用函数表达式来描述是这样的f(x) f(f(x))例如求绝对值函数。
在程序开发中则是指同一个业务执行一次或多次对业务状态的影响是一致的。例如 根据id删除数据 查询数据 新增数据 但数据的更新往往不是幂等的如果重复执行可能造成不一样的后果。比如 取消订单恢复库存的业务。如果多次恢复就会出现库存重复增加的情况 退款业务。重复退款对商家而言会有经济损失。 所以我们要尽可能避免业务被重复执行。
然而在实际业务场景中由于意外经常会出现业务被重复执行的情况例如 页面卡顿时频繁刷新导致表单重复提交 服务间调用的重试 MQ消息的重复投递 我们在用户支付成功后会发送MQ消息到交易服务修改订单状态为已支付就可能出现消息重复投递的情况。如果消费者不做判断很有可能导致消息被消费多次出现业务故障。
举例 假如用户刚刚支付完成并且投递消息到交易服务交易服务更改订单为已支付状态。 由于某种原因例如网络故障导致生产者没有得到确认隔了一段时间后重新投递给交易服务。 但是在新投递的消息被消费之前用户选择了退款将订单状态改为了已退款状态。 退款完成后新投递的消息才被消费那么订单状态会被再次改为已支付。业务异常。 因此我们必须想办法保证消息处理的幂等性。这里给出两种方案 唯一消息ID 业务状态判断 1.唯一消息ID
这个思路非常简单 每一条消息都生成一个唯一的id与消息一起投递给消费者。 消费者接收到消息后处理自己的业务业务处理成功后将消息ID保存到数据库 如果下次又收到相同消息去数据库查询判断是否存在存在则为重复消息放弃处理。 我们该如何给消息添加唯一ID呢
其实很简单SpringAMQP的MessageConverter自带了MessageID的功能我们只要开启这个功能即可。
以Jackson的消息转换器为例
Bean
public MessageConverter messageConverter(){// 1.定义消息转换器Jackson2JsonMessageConverter jjmc new Jackson2JsonMessageConverter();// 2.配置自动创建消息id用于识别不同消息也可以在业务中基于ID判断是否是重复消息jjmc.setCreateMessageIds(true);return jjmc;
}
2.业务判断
业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息不同的业务场景判断的思路也不一样。
例如我们当前案例中处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付如果不是则证明订单已经被处理过无需重复处理。 相比较而言消息ID的方案需要改造原有的数据库所以更推荐使用业务判断的方案。 4.延迟消息
在电商的支付业务中对于一些库存有限的商品为了更好的用户体验通常都会在用户下单时立刻扣减商品库存。例如电影院购票、高铁购票下单后就会锁定座位资源其他人无法重复购买。 但是这样就存在一个问题假如用户下单后一直不付款就会一直占有库存资源导致其他客户无法正常交易最终导致商户利益受损 因此电商中通常的做法就是对于超过一定时间未支付的订单应该立刻取消订单并释放占用的库存。 例如订单支付超时时间为30分钟则我们应该在用户下单后的第30分钟检查订单支付状态如果发现未支付应该立刻取消订单释放库存。 但问题来了如何才能准确的实现在下单后第30分钟去检查支付状态呢 像这种在一段时间以后才执行的任务我们称之为延迟任务而要实现延迟任务最简单的方案就是利用MQ的延迟消息了。 在RabbitMQ中实现延迟消息也有两种方案 死信交换机TTL 延迟消息插件 4.1.死信交换机和延迟消息
首先我们来学习一下基于死信交换机的延迟消息方案。
4.1.1.死信交换机
什么是死信 当一个队列中的消息满足下列情况之一时可以成为死信dead letter 消费者使用basic.reject或 basic.nack声明消费失败并且消息的requeue参数设置为false 消息是一个过期消息超时无人消费 要投递的队列消息满了无法投递 如果一个队列中的消息已经成为死信并且这个队列通过dead-letter-exchange属性指定了一个交换机那么队列中的死信就会投递到这个交换机中而这个交换机就称为死信交换机Dead Letter Exchange。而此时加入有队列与死信交换机绑定则最终死信就会被投递到这个队列中。 死信交换机有什么作用呢 收集那些因处理失败而被拒绝的消息 收集那些因队列满了而被拒绝的消息 收集因TTL有效期到期的消息 4.1.2.延迟消息
前面两种作用场景可以看做是把死信交换机当做一种消息处理的最终兜底方案与消费者重试时讲的RepublishMessageRecoverer作用类似。 而最后一种场景大家设想一下这样的场景
如图有一组绑定的交换机ttl.fanout和队列ttl.queue。但是ttl.queue没有消费者监听而是设定了死信交换机hmall.direct而队列direct.queue1则与死信交换机绑定RoutingKey是blue
假如我们现在发送一条消息到ttl.fanoutRoutingKey为blue并设置消息的有效期为5000毫秒 注意尽管这里的ttl.fanout不需要RoutingKey但是当消息变为死信并投递到死信交换机时会沿用之前的RoutingKey这样hmall.direct才能正确路由消息。 消息肯定会被投递到ttl.queue之后由于没有消费者因此消息无人消费。5秒之后消息的有效期到期成为死信 死信被再次投递到死信交换机hmall.direct并沿用之前的RoutingKey也就是blue 由于direct.queue1与hmall.direct绑定的key是blue因此最终消息被成功路由到direct.queue1如果此时有消费者与direct.queue1绑定 也就能成功消费消息了。但此时已经是5秒钟以后了 也就是说publisher发送了一条消息但最终consumer在5秒后才收到消息。我们成功实现了延迟消息。 4.1.3.总结 注意 RabbitMQ的消息过期是基于追溯方式来实现的也就是说当一个消息的TTL到期以后不一定会被移除或投递到死信交换机而是在消息恰好处于队首时才会被处理。 当队列中消息堆积很多的时候过期消息可能不会被按时处理因此你设置的TTL时间不一定准确。 4.2.取消超时订单 把30分钟的等待时间拆分成小的等待时间每隔一定时间检查一次用户是否已经完成支付。如果用户已经支付成功就可以把MQ中的消息给删除。从而大大减轻了MQ中消息的堆积数量。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/89555.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!