官方网站建设银行年利息是多少东莞网站建设信科分公司
官方网站建设银行年利息是多少,东莞网站建设信科分公司,品牌建设总结报告,开发公司移留问题解决物业如何打报告发送者可靠性 mq可靠性 消费者可靠性 延迟消息消息从生产者到消费者每一步都可能导致消息丢失发送消息时丢失#xff1a;生产者发送消息时连接MQ失败生产者发送消息到达MQ后未找到Exchange生产者发送消息到达MQ的Exchange后#xff0c;未找到合适的Queue消息到达MQ后#xf…发送者可靠性 mq可靠性 消费者可靠性 延迟消息消息从生产者到消费者每一步都可能导致消息丢失发送消息时丢失生产者发送消息时连接MQ失败生产者发送消息到达MQ后未找到Exchange生产者发送消息到达MQ的Exchange后未找到合适的Queue消息到达MQ后处理消息的进程发生异常MQ导致消息丢失消息到达MQ保存到队列后尚未消费就突然宕机消费者处理消息时消息接收后尚未处理突然宕机消息接收后处理过程中抛出异常保证消息的可靠性从三方面进行入手确保生产者一定把消息发送到MQ确保MQ不会将消息弄丢确保消费者一定要处理消息发送者可靠性1生产者可靠性 生产者重试机制但是当网络不稳定的时候利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试也就是说多次重试等待的过程中当前线程是被阻塞的。如果对于业务性能有要求建议禁用重试机制。如果一定要使用请合理配置等待时长和重试次数当然也可以考虑使用异步线程来执行发送消息的代码。具体yml文件logging: pattern: dateformat: MM-dd HH:mm:ss:SSS level: com.itheima: DEBUG #设置com.itheima包及其子包的日志级别为DEBUG可以输出更多的调试信息 spring: rabbitmq: host: localhost port: 5672 virtual-host: / username: admin password: password123 connection-timeout: 1s #连接超时时间单位为毫秒 template: retry: enabled: true #启用重试机制 multiplier: 1 #重试间隔的倍数默认为1 这个只是连接失败的重试 max-attempts: 3 #最大重试次数默认为3 publisher-confirm-type: none #correlated #启用消息确认机制使用相关模式可以通过回调函数获取确认结果 publisher-returns: false #true #启用消息返回机制当消息无法路由到队列时会触发返回生产者确认机制:一般情况下只要生产者与MQ之间的网路连接顺畅基本不会出现发送消息丢失的情况因此大多数情况下我们无需考虑这种问题。不过在少数情况下也会出现消息发送到MQ之后丢失的现象比如- MQ内部处理消息的进程发生了异常- 生产者发送消息到达MQ后未找到Exchange- 生产者发送消息到达MQ的Exchange后未找到合适的Queue因此无法路由针对上述情况RabbitMQ提供了生产者消息确认机制包括Publisher Confirm和Publisher Return两种。在开启确认机制的情况下当生产者发送消息给MQ后MQ会根据消息处理的情况返回不同的**回执**。- 当消息投递到MQ但是路由失败时通过**Publisher Return**返回异常信息同时返回ack的确认信息代表投递成功- 临时消息投递到了MQ并且入队成功返回ACK告知投递成功- 持久消息投递到了MQ并且入队完成持久化返回ACK 告知投递成功- 其它情况都会返回NACK告知投递失败其中ack和nack属于**Publisher Confirm**机制ack是投递成功nack是投递失败。而return则属于**Publisher Return**机制。默认两种机制都是关闭状态需要通过配置文件来开启。具体的yml文件配置如上图所示这里publisher-confirm-type有三种模式可选none关闭confirm机制simple同步阻塞等待MQ的回执correlatedMQ异步回调返回回执 已不是采用这种回调方式配置ReturnCallBackSlf4j Configuration AllArgsConstructor public class MqConfirmConfig implements ApplicationContextAware {//实现这个ApplicationContextAware接口在进行执行setApplicationContjjext将整个上下文进行传进去 Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { RabbitTemplate rabbitTemplate applicationContext.getBean(RabbitTemplate.class);//手动获得到rabbittemplate的bean //配置return回调 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnsCallback() { Override public void returnedMessage(ReturnedMessage returnedMessage) { log.debug(收到的消息return 回调了,exchange:{},routingKey:{},replyCode:{},replyText:{},message:{}, returnedMessage.getExchange(), returnedMessage.getRoutingKey(), returnedMessage.getReplyCode(), returnedMessage.getReplyText(), returnedMessage.getMessage() ); } }); } }定义ConfirmCallBackTest void testConfirm() throws InterruptedException { //创建cd CorrelationData 是 Spring AMQP 用来追踪消息发送状态的容器。 CorrelationData cd new CorrelationData(UUID.randomUUID().toString()); //添加回调 cd.getFuture().addCallback(new ListenableFutureCallbackCorrelationData.Confirm() { Override public void onFailure(Throwable ex) { log.error(消息回调失败, ex); }//保证消息发送成功后才会执行onSuccess方法如果消息发送失败了就会执行onFailure方法 Override public void onSuccess(Nullable CorrelationData.Confirm result) { log.debug(消息回调成功结果{}, result); if (result.isAck()) { //消息发送成功 log.debug(发送消息成功收到ack回调); } else { //发送消息失败 log.error(发送消息失败收到ack回调原因{}, result.getReason()); } } }); rabbitTemplate.convertAndSend(test1.direct1, red, hello, cd); /* cd.getFuture()获取一个 ListenableFuture 对象。这是一个异步任务句柄代表“等待 RabbitMQ 返回确认结果”这个未来的动作。 addCallback给这个未来动作绑定两个回调函数 onFailure(Throwable ex) 触发时机当发送过程本身出现异常时触发。 场景例如网络断了、RabbitMQ 服务挂了、连接超时等导致消息根本没能发出去或者连确认都没收到就报错了。 注意这不代表消息在 MQ 内部处理失败而是通信层面的失败。 onSuccess(CorrelationData.Confirm result) 触发时机当成功收到 RabbitMQ 的确认帧Ack/Nack时触发。 参数 result包含确认的具体结果。 */ }由于传递的RoutingKey是错误的路由失败后触发了return callback同时也收到了ack。当我们修改为正确的RoutingKey以后就不会触发return callback了只收到ack。而如果连交换机都是错误的则只会收到nack。注意开启生产者确认比较消耗MQ性能一般不建议开启。而且大家思考一下触发确认的几种情况路由失败一般是因为RoutingKey错误导致往往是编程导致交换机名称错误同样是编程错误导致MQ内部故障这种需要处理但概率往往较低。因此只有对消息可靠性要求非常高的业务才需要开启而且仅仅需要开启ConfirmCallback处理nack就可以了。2,MQ的可靠性为了提升性能默认情况下MQ的数据都是在内存存储的临时数据重启后就会消失。为了保证数据的可靠性必须配置数据持久化包括交换机持久化 在控制台的Exchanges页面添加交换机时可以配置交换机的Durability参数设置为Durable就是持久化模式Transient就是临时模式。队列持久化 在控制台的Queues页面添加队列时同样可以配置队列的Durability参数除了持久化以外你可以看到队列还有很多其它参数有一些我们会在后期学习。消息持久化 在控制台发送消息的时候可以添加很多参数而消息的持久化是要配置一个propertiesTest void testPageOut() { Message msg MessageBuilder.withBody(hello.getBytes(StandardCharsets.UTF_8)) .setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();//设置消息持久化 rabbitTemplate.convertAndSend(fanout.queue2, msg); /* 在开启持久化机制以后如果同时还开启了生产者确认那么MQ会在消息持久化以后才发送ACK回执进一步确保消息的可靠性。 不过出于性能考虑为了减少IO次数发送到MQ的消息并不是逐条持久化到数据库的而是每隔一段时间批量持久化。一般间隔在100毫秒左右这就会导致ACK有一定的延迟因此建议生产者确认全部采用异步方式。 */ }LazyQueue在默认情况下RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。但在某些特殊情况下这会导致消息积压比如消费者宕机或出现网络故障消息发送量激增超过了消费者处理速度消费者处理业务发生阻塞一旦出现消息堆积问题RabbitMQ的内存占用就会越来越高直到触发内存预警上限。此时RabbitMQ会将内存消息刷到磁盘上这个行为成为PageOut.PageOut会耗费一段时间并且会阻塞队列进程。因此在这个过程中RabbitMQ不会再处理新的消息生产者的所有请求都会被阻塞。为了解决这个问题从RabbitMQ的3.6.0版本开始就增加了Lazy Queues的模式也就是惰性队列。惰性队列的特征如下接收到消息后直接存入磁盘而非内存消费者要消费消息时才会从磁盘中读取并加载到内存也就是懒加载支持数百万条的消息存储而在3.12版本之后LazyQueue已经成为所有队列的默认格式。因此官方推荐升级MQ为3.12版本或者所有队列都设置为LazyQueue模式。3,保证消费者的可靠性当RabbitMQ向消费者投递消息以后需要知道消费者的处理状态如何。因为消息投递给消费者并不代表就一定被正确消费了可能出现的故障有很多比如消息投递的过程中出现了网络故障消费者接收到消息后突然宕机消费者接收到消息后因处理不当导致异常一旦发生上述情况消息也会丢失。因此RabbitMQ必须知道消费者的处理状态一旦消息处理失败才能重新投递消息。消费者确认机制为了确认消费者是否成功处理消息RabbitMQ提供了消费者确认机制Consumer Acknowledgement。即当消费者处理消息结束后应该向RabbitMQ发送一个回执告知RabbitMQ自己消息处理状态。回执有三种可选值ack成功处理消息RabbitMQ从队列中删除该消息nack消息处理失败RabbitMQ需要再次投递消息reject消息处理失败并拒绝该消息RabbitMQ从队列中删除该消息一般reject方式用的较少除非是消息格式有问题那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过try catch机制捕获消息处理成功时返回ack处理失败时返回nack.由于消息回执的处理代码比较统一因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式有三种模式**none**不处理。即消息投递给消费者后立刻ack消息会立刻从MQ删除。非常不安全不建议使用**manual**手动模式。需要自己在业务代码中调用api发送ack或reject存在业务入侵但更灵活**auto**自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强当业务正常执行时则自动返回ack. 当业务出现异常时根据异常判断返回不同结果如果是业务异常会自动返回nack如果是消息处理或校验异常自动返回reject;具体消费者的配置文件logging: pattern: dateformat: MM-dd HH:mm:ss:SSS spring: rabbitmq: host: localhost port: 5672 virtual-host: / username: admin password: password123 listener: simple: prefetch: 1 #每次只能消费一条消息直到处理完成后才会接收下一条消息 retry: enabled: true #启用重试机制 acknowledge-mode: auto #确认机制none表示不需要确认auto表示自动确认manual表示手动确认失败处理策略在之前的测试中本地测试达到最大重试次数后消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下显然不太合适了。因此Spring允许我们自定义重试次数耗尽后的消息处理策略这个策略是由MessageRecovery接口来定义的它有3个不同实现RejectAndDontRequeueRecoverer重试耗尽后直接reject丢弃消息。默认就是这种方式ImmediateRequeueMessageRecoverer重试耗尽后返回nack消息重新入队RepublishMessageRecoverer重试耗尽后将失败消息投递到指定的交换机比较优雅的一种处理方案是RepublishMessageRecoverer失败后将消息投递到一个指定的专门存放异常消息的队列后续由人工集中处理。1在consumer服务中定义处理失败消息的交换机和队列Bean public DirectExchange errorMessageExchange(){ return new DirectExchange(error.direct); } Bean public Queue errorQueue(){ return new Queue(error.queue, true); } Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with(error); }2定义一个RepublishMessageRecoverer关联队列和交换机Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, error.direct, error); }完整代码如下Configuration ConditionalOnProperty(prefix spring.rabbitmq.listener.simple.retry,name enabled,havingValue true) public class ErrorConfiguration { Bean public DirectExchange errorExchange(){ return new DirectExchange(error.direct); } Bean public Queue errorQueue(){ return new Queue(error.queue); } Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorExchange){ return BindingBuilder.bind(errorQueue).to(errorExchange).with(error); } Bean public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate,error.direct,error);//重新发布 } }保持业务幂等性业务幂等性何为幂等性幂等是一个数学概念用函数表达式来描述是这样的f(x) f(f(x))例如求绝对值函数。在程序开发中则是指同一个业务执行一次或多次对业务状态的影响是一致的。例如根据id删除数据查询数据新增数据但数据的更新往往不是幂等的如果重复执行可能造成不一样的后果。比如取消订单恢复库存的业务。如果多次恢复就会出现库存重复增加的情况退款业务。重复退款对商家而言会有经济损失。所以我们要尽可能避免业务被重复执行。然而在实际业务场景中由于意外经常会出现业务被重复执行的情况例如页面卡顿时频繁刷新导致表单重复提交服务间调用的重试MQ消息的重复投递我们在用户支付成功后会发送MQ消息到交易服务修改订单状态为已支付就可能出现消息重复投递的情况。如果消费者不做判断很有可能导致消息被消费多次出现业务故障。举例假如用户刚刚支付完成并且投递消息到交易服务交易服务更改订单为已支付状态。由于某种原因例如网络故障导致生产者没有得到确认隔了一段时间后重新投递给交易服务。但是在新投递的消息被消费之前用户选择了退款将订单状态改为了已退款状态。退款完成后新投递的消息才被消费那么订单状态会被再次改为已支付。业务异常。因此我们必须想办法保证消息处理的幂等性。这里给出两种方案唯一消息ID业务状态判断唯一消息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; }业务判断业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息不同的业务场景判断的思路也不一样。例如我们当前案例中处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付如果不是则证明订单已经被处理过无需重复处理。相比较而言消息ID的方案需要改造原有的数据库所以我更推荐使用业务判断的方案。以支付修改订单的业务为例我们需要修改OrderServiceImpl中的markOrderPaySuccess方法Override public void markOrderPaySuccess(Long orderId) { // 1.查询订单 Order old getById(orderId); // 2.判断订单状态 if (old null || old.getStatus() ! 1) { // 订单不存在或者订单状态不是1放弃处理 return; } // 3.尝试更新订单 Order order new Order(); order.setId(orderId); order.setStatus(2); order.setPayTime(LocalDateTime.now()); updateById(order); }上述代码逻辑上符合了幂等判断的需求但是由于判断和更新是两步动作因此在极小概率下可能存在线程安全问题。我们可以合并上述操作为这样Override public void markOrderPaySuccess(Long orderId) { // UPDATE order SET status ? , pay_time ? WHERE id ? AND status 1 lambdaUpdate() .set(Order::getStatus, 2) .set(Order::getPayTime, LocalDateTime.now()) .eq(Order::getId, orderId) .eq(Order::getStatus, 1) .update(); }注意看上述代码等同于这样的SQL语句UPDATE order SET status ? , pay_time ? WHERE id ? AND status 1我们在where条件中除了判断id以外还加上了status必须为1的条件。如果条件不符说明订单已支付则SQL匹配不到数据根本不会执行。保持原子性兜底方案虽然我们利用各种机制尽可能增加了消息的可靠性但也不好说能保证消息100%的可靠。万一真的MQ通知失败该怎么办呢有没有其它兜底方案能够确保订单的支付状态一致呢其实思想很简单既然MQ通知不一定发送到交易服务那么交易服务就必须自己主动去查询支付状态。这样即便支付服务的MQ通知失败我们依然能通过主动查询来保证订单状态的一致。这个时间是无法确定的因此通常我们采取的措施就是利用定时任务定期查询例如每隔20秒就查询一次并判断支付状态。如果发现订单已经支付则立刻更新订单状态为已支付即可。至此消息可靠性的问题已经解决了。综上支付服务与交易服务之间的订单状态一致性是如何保证的首先支付服务会正在用户支付成功以后利用MQ消息通知交易服务完成订单状态同步。其次为了保证MQ消息的可靠性我们采用了生产者确认机制、消费者确认、消费者失败重试等策略确保消息投递的可靠性最后我们还在交易服务设置了定时任务定期查询订单支付状态。这样即便MQ通知失败还可以利用定时任务作为兜底方案确保订单支付状态的最终一致性。延迟消息在电商的支付业务中对于一些库存有限的商品为了更好的用户体验通常都会在用户下单时立刻扣减商品库存。例如电影院购票、高铁购票下单后就会锁定座位资源其他人无法重复购买。但是这样就存在一个问题假如用户下单后一直不付款就会一直占有库存资源导致其他客户无法正常交易最终导致商户利益受损因此电商中通常的做法就是对于超过一定时间未支付的订单应该立刻取消订单并释放占用的库存。例如订单支付超时时间为30分钟则我们应该在用户下单后的第30分钟检查订单支付状态如果发现未支付应该立刻取消订单释放库存。但问题来了如何才能准确的实现在下单后第30分钟去检查支付状态呢像这种在一段时间以后才执行的任务我们称之为延迟任务而要实现延迟任务最简单的方案就是利用MQ的延迟消息了。在RabbitMQ中实现延迟消息也有两种方案死信交换机TTL延迟消息插件死信交换机和延迟消息首先我们来学习一下基于死信交换机的延迟消息方案。死信交换机什么是死信当一个队列中的消息满足下列情况之一时可以成为死信dead letter消费者使用basic.reject或basic.nack声明消费失败并且消息的requeue参数设置为false消息是一个过期消息超时无人消费要投递的队列消息满了无法投递如果一个队列中的消息已经成为死信并且这个队列通过**dead-letter-exchange**属性指定了一个交换机那么队列中的死信就会投递到这个交换机中而这个交换机就称为死信交换机Dead Letter Exchange。而此时加入有队列与死信交换机绑定则最终死信就会被投递到这个队列中。死信交换机有什么作用呢收集那些因处理失败而被拒绝的消息收集那些因队列满了而被拒绝的消息收集因TTL有效期到期的消息延迟消息前面两种作用场景可以看做是把死信交换机当做一种消息处理的最终兜底方案与消费者重试时讲的RepublishMessageRecoverer作用类似。Test void testSendTTLMessage () { rabbitTemplate.convertAndSend(simple.direct, hi, hello, new MessagePostProcessor() { Override public Message postProcessMessage(Message message) throws AmqpException { //使用消息转换器 message.getMessageProperties().setExpiration(10000); return message; } }); log.info(消息发送成功); }注意RabbitMQ的消息过期是基于追溯方式来实现的也就是说当一个消息的TTL到期以后不一定会被移除或投递到死信交换机而是在消息恰好处于队首时才会被处理。当队列中消息堆积很多的时候过期消息可能不会被按时处理因此你设置的TTL时间不一定准确。声明延迟交换机基于注解方式RabbitListener(bindings QueueBinding( value Queue(name delay.queue, durable true), exchange Exchange(name delay.direct, delayed true), key delay )) public void listenDelayMessage(String msg){ log.info(接收到delay.queue的延迟消息{}, msg); }基于Bean的方式package com.itheima.consumer.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Slf4j Configuration public class DelayExchangeConfig { Bean public DirectExchange delayExchange(){ return ExchangeBuilder .directExchange(delay.direct) // 指定交换机类型和名称 .delayed() // 设置delay的属性为true .durable(true) // 持久化 .build(); } Bean public Queue delayedQueue(){ return new Queue(delay.queue); } Bean public Binding delayQueueBinding(){ return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with(delay); } }发送延迟消息发送消息时必须通过x-delay属性设定延迟时间Test void testPublisherDelayMessage() { // 1.创建消息 String message hello, delayed message; // 2.发送消息利用消息后置处理器添加消息头 rabbitTemplate.convertAndSend(delay.direct, delay, message, new MessagePostProcessor() { Override public Message postProcessMessage(Message message) throws AmqpException { // 添加延迟消息属性 message.getMessageProperties().setDelay(5000); return message; } }); }注意延迟消息插件内部会维护一个本地数据库表同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长可能会导致堆积的延迟消息非常多会带来较大的CPU开销同时延迟消息的时间会存在误差。因此不建议设置延迟时间过长的延迟消息。订单状态同步问题假如订单超时支付时间为30分钟理论上说我们应该在下单时发送一条延迟消息延迟时间为30分钟。这样就可以在接收到消息时检验订单支付状态关闭未支付订单。但是大多数情况下用户支付都会在1分钟内完成我们发送的消息却要在MQ中停留30分钟额外消耗了MQ的资源。因此我们最好多检测几次订单支付状态而不是在最后第30分钟才检测。例如我们在用户下单后的第10秒、20秒、30秒、45秒、60秒、1分30秒、2分、...30分分别设置延迟消息如果提前发现订单已经支付则后续的检测取消即可。这样就可以有效避免对MQ资源的浪费了。package com.hmall.common.domain; import com.hmall.common.utils.CollUtils; import lombok.Data; import java.util.List; Data public class MultiDelayMessageT { /** * 消息体 */ private T data; /** * 记录延迟时间的集合 */ private ListLong delayMillis; public MultiDelayMessage(T data, ListLong delayMillis) { this.data data; this.delayMillis delayMillis; } public static T MultiDelayMessageT of(T data, Long ... delayMillis){ return new MultiDelayMessage(data, CollUtils.newArrayList(delayMillis)); } /** * 获取并移除下一个延迟时间 * return 队列中的第一个延迟时间 */ public Long removeNextDelay(){ return delayMillis.remove(0); } /** * 是否还有下一个延迟时间 */ public boolean hasNextDelay(){ return !delayMillis.isEmpty(); } }