为什么大家都在抵制用定时任务实现「关闭超时订单」功能?

e355cd44a9ee7073c8a5ef63b0ffd75e.gif

作者 | 阿Q

来源 | 阿Q说代码

前几天领导突然宣布几年前停用的电商项目又重新启动了,让我把代码重构下进行升级。

让我最深恶痛觉的就是里边竟然用定时任务实现了“关闭超时订单”的功能,现在想来,哭笑不得。我们先分析一波为什么大家都在抵制用定时任务来实现该功能。

定时任务

关闭超时订单是在创建订单之后的一段时间内未完成支付而关闭订单的操作,该功能一般要求每笔订单的超时时间是一致的。

如果我们使用定时任务来进行该操作,很难把握定时任务轮询的时间间隔:

  • 时间间隔足够小,在误差允许的范围内可以达到我们说的时间一致性问题,但是频繁扫描数据库,执行定时任务,会造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击;

  • 时间间隔比较大,由于每个订单创建的时间不一致,所以上边的一致性要求很难达到,举例如下:

add46df543a672fef403cb49a6438409.png

假设30分钟订单超时自动关闭,定时任务的执行间隔时间为30分钟:

  1. 我们在第5分钟进行下单操作;

  2. 当时间来到第30分钟时,定时任务执行一次,但是我们的订单未满足条件,不执行;

  3. 当时间来到第35分钟时,订单达到关闭条件,但是定时任务未执行,所以不执行;

  4. 当时间来到第60分钟时,开始执行我们的订单关闭操作,而此时,误差达到25分钟。

经此种种,我们需要舍弃该方式。

延时队列

为了满足领导的需求,我便将手伸向了消息队列:RabbitMQ。尽管它本身并没有提供延时队列的功能,但是我们可以利用它的存活时间和死信交换机的特性来间接实现。

首先我们先来简单介绍下什么是存活时间?什么是死信交换机?

存活时间

存活时间的全拼是Time To Live,简称 TTL。它既支持对消息本身进行设置(延迟队列的关键),又支持对队列进行设置(该队列中所有消息存在相同的过期时间)。

  • 对消息本身进行设置:即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的;

  • 对队列进行设置:一旦消息过期,就会从队列中抹去;

如果同时使用这两种方法,那么以过期时间的那个数值为准。当消息达到过期时间还没有被消费,那么该消息就“死了”,我们把它称为 死信 消息。

消息变为死信的条件:

  • 消息被拒绝(basic.reject/basic.nack),并且requeue=false;

  • 消息的过期时间到期了;

  • 队列达到最大长度;

队列设置注意事项

  1. 队列中该属性的设置要在第一次声明队列的时候设置才有效,如果队列一开始已存在且没有这个属性,则要删掉队列再重新声明才可以;

  2. 队列的 ttl 只能被设置为某个固定的值,一旦设置后则不能更改,否则会抛出异常;

死信交换机

死信交换机全拼Dead-Letter-Exchange,简称DLX

当消息在一个队列中变成死信之后,如果这个消息所在的队列设置了x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换机上,这个交换机就称之为死信交换机,与这个死信交换器绑定的队列就是死信队列。

  • x-dead-letter-exchange:出现死信之后将死信重新发送到指定交换机;

  • x-dead-letter-routing-key:出现死信之后将死信重新按照指定的routing-key发送,如果不设置默认使用消息本身的routing-key

死信队列与普通队列的区别就是它的RoutingKeyExchange需要作为参数,绑定到正常的队列上。

实战教学

先来张图感受下我们的整体思路

f724baa2819b6b441b9dfaf827394a91.png

  1. 生产者发送带有 ttl 的消息放入交换机路由到延时队列中;

  2. 在延时队列中绑定死信交换机与死信转发的routing-key

  3. 等延时队列中的消息达到延时时间之后变成死信转发到死信交换机并路由到死信队列中;

  4. 最后供消费者消费。

代码实现:

配置类

@Configuration
public class DelayQueueRabbitConfig {public static final String DLX_QUEUE = "queue.dlx";//死信队列public static final String DLX_EXCHANGE = "exchange.dlx";//死信交换机public static final String DLX_ROUTING_KEY = "routingkey.dlx";//死信队列与死信交换机绑定的routing-keypublic static final String ORDER_QUEUE = "queue.order";//订单的延时队列public static final String ORDER_EXCHANGE = "exchange.order";//订单交换机public static final String ORDER_ROUTING_KEY = "routingkey.order";//延时队列与订单交换机绑定的routing-key/*** 定义死信队列**/@Beanpublic Queue dlxQueue(){return new Queue(DLX_QUEUE,true);}/*** 定义死信交换机**/@Beanpublic DirectExchange dlxExchange(){return new DirectExchange(DLX_EXCHANGE, true, false);}/*** 死信队列和死信交换机绑定* 设置路由键:routingkey.dlx**/@BeanBinding bindingDLX(){return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(DLX_ROUTING_KEY);}/*** 订单延时队列* 设置队列里的死信转发到的DLX名称* 设置死信在转发时携带的 routing-key 名称**/@Beanpublic Queue orderQueue() {Map<String, Object> params = new HashMap<>();params.put("x-dead-letter-exchange", DLX_EXCHANGE);params.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);return new Queue(ORDER_QUEUE, true, false, false, params);}/*** 订单交换机**/@Beanpublic DirectExchange orderExchange() {return new DirectExchange(ORDER_EXCHANGE, true, false);}/*** 把订单队列和订单交换机绑定在一起**/@Beanpublic Binding orderBinding() {return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(ORDER_ROUTING_KEY);}
}

发送消息

@RequestMapping("/order")
public class OrderSendMessageController {@Autowiredprivate RabbitTemplate rabbitTemplate;@GetMapping("/sendMessage")public String sendMessage(){String delayTime = "10000";//将消息携带路由键值rabbitTemplate.convertAndSend(DelayQueueRabbitConfig.ORDER_EXCHANGE, DelayQueueRabbitConfig.ORDER_ROUTING_KEY,"发送消息!",message->{message.getMessageProperties().setExpiration(delayTime);return message;});return "ok";}}

消费消息

@Component
@RabbitListener(queues = DelayQueueRabbitConfig.DLX_QUEUE)//监听队列名称
public class OrderMQReciever {@RabbitHandlerpublic void process(String message){System.out.println("OrderMQReciever接收到的消息是:"+ message);}
}

测试

通过调用接口,发现10秒之后才会消费消息

3ff197d048f3f415b53ab5bc2c5d2aab.png

问题升级

由于开发环境和测试环境使用的是同一个交换机和队列,所以发送的延时时间都是30分钟。但是为了在测试环境让测试同学方便测试,故手动将测试环境的时间改为了1分钟。

问题复现

接着问题就来了:延时时间为1分钟的消息并没有立即被消费,而是等30分钟的消息被消费完之后才被消费了。至于原因,我们下边再分析,先用代码来给大家复现下该问题。

@GetMapping("/sendManyMessage")
public String sendManyMessage(){send("延迟消息睡10秒",10000+"");send("延迟消息睡2秒",2000+"");send("延迟消息睡5秒",5000+"");return "ok";
}private void send(String msg, String delayTime){rabbitTemplate.convertAndSend(DelayQueueRabbitConfig.ORDER_EXCHANGE, DelayQueueRabbitConfig.ORDER_ROUTING_KEY,msg,message->{message.getMessageProperties().setExpiration(delayTime);return message;});
}

执行结果如下:

OrderMQReciever接收到的消息是:延迟消息睡10秒
OrderMQReciever接收到的消息是:延迟消息睡2秒
OrderMQReciever接收到的消息是:延迟消息睡5秒

原因就是延时队列也满足队列先进先出的特征,当10秒的消息未出队列时,后边的消息不能顺利出队,造成后边的消息阻塞了,未能达到精准延时。

问题解决

我们可以利用x-delay-message插件来解决该问题

消息的延迟范围是 Delay > 0, Delay =< ?ERL_MAX_T(在 Erlang 中可以被设置的范围为 (2^32)-1 毫秒)

5978fcceb85863e16e289fa860f871b7.png

  1. 生产者发送消息到交换机时,并不会立即进入,而是先将消息持久化到 Mnesia(一个分布式数据库管理系统);

  2. 插件将会尝试确认消息是否过期;

  3. 如果消息过期,消息会通过 x-delayed-type 类型标记的交换机投递至目标队列,供消费者消费;

实践

我这边使用的是v3.8.0.ez,将文件下载下来放到服务器的/usr/local/soft/rabbitmq_server-3.7.14/plugins 路径下,执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange命令即可。

6574261345ee0914c6cc779659177819.pnge28a5c5f39e4b2f46c09315e59b79793.png

出现如图所示,代表安装成功。

配置类

@Configuration
public class XDelayedMessageConfig {public static final String DIRECT_QUEUE = "queue.direct";//队列public static final String DELAYED_EXCHANGE = "exchange.delayed";//延迟交换机public static final String ROUTING_KEY = "routingkey.bind";//绑定的routing-key/*** 定义队列**/@Beanpublic Queue directQueue(){return new Queue(DIRECT_QUEUE,true);}/*** 定义延迟交换机* args:根据该参数进行灵活路由,设置为“direct”,意味着该插件具有与直连交换机具有相同的路由行为,* 如果想要不同的路由行为,可以更换现有的交换类型如:“topic”* 交换机类型为 x-delayed-message**/@Beanpublic CustomExchange delayedExchange(){Map<String, Object> args = new HashMap<String, Object>();args.put("x-delayed-type", "direct");return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);}/*** 队列和延迟交换机绑定**/@Beanpublic Binding orderBinding() {return BindingBuilder.bind(directQueue()).to(delayedExchange()).with(ROUTING_KEY).noargs();}}

发送消息

@RestController
@RequestMapping("/delayed")
public class DelayedSendMessageController {@Autowiredprivate RabbitTemplate rabbitTemplate;@GetMapping("/sendManyMessage")public String sendManyMessage(){send("延迟消息睡10秒",10000);send("延迟消息睡2秒",2000);send("延迟消息睡5秒",5000);return "ok";}private void send(String msg, Integer delayTime){//将消息携带路由键值rabbitTemplate.convertAndSend(XDelayedMessageConfig.DELAYED_EXCHANGE,XDelayedMessageConfig.ROUTING_KEY,msg,message->{message.getMessageProperties().setDelay(delayTime);return message;});}
}

消费消息

@Component
@RabbitListener(queues = XDelayedMessageConfig.DIRECT_QUEUE)//监听队列名称
public class DelayedMQReciever {@RabbitHandlerpublic void process(String message){System.out.println("DelayedMQReciever接收到的消息是:"+ message);}
}

测试

DelayedMQReciever接收到的消息是:延迟消息睡2秒
DelayedMQReciever接收到的消息是:延迟消息睡5秒
DelayedMQReciever接收到的消息是:延迟消息睡10秒

这样我们的问题就顺利解决了。

局限性

延迟的消息存储在一个Mnesia表中,当前节点上只有一个磁盘副本,它们将在节点重启后存活。

虽然触发计划交付的计时器不会持久化,但它将在节点启动时的插件激活期间重新初始化。显然,集群中只有一个预定消息的副本意味着丢失该节点或禁用其上的插件将丢失驻留在该节点上的消息。

该插件的当前设计并不适合延迟消息数量较多的场景(如数万条或数百万条),另外该插件的一个可变性来源是依赖于 Erlang 计时器,在系统中使用了一定数量的长时间计时器之后,它们开始争用调度程序资源,并且时间漂移不断累积。

9e85b00feead7789c11095610a36b9b3.gif

往期推荐

从 40% 跌至 4%,“糊”了的 Firefox 还能重回巅峰吗?

Gartner 发布 2022 年汽车行业五大技术趋势

别再用 Redis List 实现消息队列了,Stream 专为队列而生

漫画:什么是“低代码”开发平台?

8f2a60e30dd1055f2b650baf486e03f0.gif

点分享

8974d72a0904cf4319aefc2d29133b02.gif

点收藏

b45a9b6451ed5d11f84ceda8a4e67179.gif

点点赞

7fdb3612a3be4a579c6ef3aa6c13b1c4.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/512233.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面对疾风吧,如何搭建高协同的精准告警体系?

简介&#xff1a; 想要实现AiOps&#xff0c;智能告警少不了。Arms 告警运维中心让面向告警的组织协同更加便捷高效&#xff01; 作者&#xff5c;九辩 世上没有一个系统是百分之百尽善尽美的。如果想要保证可用性&#xff0c;那么技术团队就得对服务的各种状态了如指掌&…

KubeMeet|聊聊新锐开源项目与云原生新的价值聚焦点

简介&#xff1a; 10 月 16 日上海&#xff0c;OAM/KubeVela、OpenKruise、OCM 三大开源项目的社区负责人、核心贡献者和企业用户将齐聚 KubeMeet&#xff0c;和现场 100 名开发者聊聊新的技术环境和企业需求下&#xff0c;有关“云原生应用管理”的那些事儿。 随着云原生关注…

Redis 究竟适不适合当队列来用?

‍作者 | Magic Kaito来源 | 水滴与银弹我经常听到很多人讨论&#xff0c;关于「把 Redis 当作队列来用是否合适」的问题。有些人表示赞成&#xff0c;他们认为 Redis 很轻量&#xff0c;用作队列很方便。也些人则反对&#xff0c;认为 Redis 会「丢」数据&#xff0c;最好还是…

EDA 事件驱动架构与 EventBridge 二三事

简介&#xff1a; 事件驱动型架构 (EDA) 方兴未艾&#xff0c;作为一种 Serverless 化的应用概念对云原生架构具有着深远影响。当我们讨论到一个具体架构时&#xff0c;首当其冲的是它的发展是否具有技术先进性。这里从我们熟悉的 MVC 架构&#xff0c;SOA 架构谈起&#xff0c…

如果被问到分布式锁,应该怎样回答?

作者 | tech-bus.七十一来源 | 程序员巴士说到锁&#xff0c;在平时的工作中&#xff0c;主要是使用synchronized关键字&#xff0c;或者相关的一些类库来实现同步&#xff0c;但这都是基于单机应用而言的&#xff0c;当我们的应用多实例部署时&#xff0c;这时候就需要用到分布…

工业视觉智能实战经验之IVI算法框架2.0

简介&#xff1a; 工业视觉智能团队在交付了多个工业视觉智能质检项目后&#xff0c;发现了工业视觉智能的共性问题和解法&#xff0c;打造了工业视觉智能平台&#xff0c;通过平台的方式积累和提升工业视觉的通用能力。在平台建设上最核心的能力是算法能力。算法能力包括不断增…

技术干货 | jsAPI 方式下的导航栏的动态化修改

简介&#xff1a; 操作指导&#xff1a;通过 jsAPI 实现导航栏的动态修改。 很多开发同学在接入 H5 容器后都会对容器的导航栏进行深度定制&#xff0c;除了 Native 的定制化之外&#xff0c;还有很多场景是使用到 jsAPI 的方式&#xff0c;通过 jsAPI 实现导航栏的动态修改。 …

Gartner:企业机构需重新定义网络安全领导者角色

编辑 | 宋慧 供稿 | Gartner 根据Gartner的最新调查&#xff0c;由于网络风险责任已被转移到IT以外&#xff0c;并且日益分散的生态系统导致网络安全领导者正在失去对决策的直接控制权&#xff0c;企业机构需要重新定义网络安全领导者的角色。 如今&#xff0c;安全和风险管理…

成本直降50%,下一代网关震撼发布

简介&#xff1a; 在容器和K8s主导的云原生时代&#xff0c;网关的新形态变得逐渐清晰&#xff0c;阿里内部也孵化出了下一代的网关产品 - 云原生网关&#xff0c;已在支付宝、淘宝、优酷、口碑等业务成功上线&#xff0c;并且经历了2020双11大促海量请求的考验&#xff0c;目前…

备战“双11”,阿里云为企业提供一站式资源保障服务

简介&#xff1a; 阿里云弹性计算将上线资源保障服务&#xff0c;通过智能化资源诊断、推荐、资源预定及授权候补为用户提供一站式自助化资源保障服务&#xff0c;兼顾灵活&#xff0c;经济的同时还能获得时刻的确定性保障&#xff0c;为业务顺畅前行保驾护航。 报名体验资源保…

快速上手 Serverless | 入门第一课

简介&#xff1a; 本文从云计算抛砖引玉&#xff0c;详解 Serverless 的典型应用场景和一些产品介绍。 一、 从云计算到 Serverless 自世界上第一台通用计算机 ENIAC (图左)诞生以来&#xff0c;计算机科学与技术的发展就从未停止过前进的脚步。2003年-2006年&#xff0c;谷歌…

钉钉宜搭邵磊:钉钉宜搭低代码加速业务互联 让改变发生

简介&#xff1a; 近日&#xff0c;在2021“低代码技术发展与应用线上研讨会”上&#xff0c;钉钉宜搭产品总监邵磊带来了“钉钉宜搭低代码加速业务互联 让改变发生”的主题演讲&#xff0c;详细介绍了钉钉宜搭低代码产品的六大互联能力。 宜搭是今年1月份正式上线到钉钉&…

Cloudera发布全球企业数据成熟度报告,混合云趋势中有效数据战略是关键

编辑 | 宋慧 出品 | CSDN云计算 2022年3月初&#xff0c;企业数据云公司Cloudera近日发布与技术市场研究公司Vanson Bourne联合编写的全球企业数据战略研究报告&#xff0c;报告分别洞察了数据的使用和价值、企业数据战略、企业数据发展趋势、企业业务计划四大部分的内容&…

基于海量日志和时序数据的质量建设最佳实践

简介&#xff1a; 在云原生和DevOps研发模式的挑战下&#xff0c;一个系统从开发、测试、到上线的整个过程中&#xff0c;会产生大量的日志、指标、事件以及告警等数据&#xff0c;这也给企业质量平台建设带来了很大的挑战。本议题主要通过可观测性的角度来讨论基于海量日志和时…

阿里云RDS深度定制-XA Crash Safe

简介&#xff1a; 近几年&#xff0c;随着分布式数据库系统的兴起&#xff0c;特别是基于MySQL分布式数据库系统&#xff0c;会用到XA来保证全局事务的一致性。众所周知&#xff0c;MySQL对XA事务的支持是比较弱的&#xff0c;存在很多问题。为了满足分布式数据库系统对XA事务的…

java集合表_java集合类散列表

哈希表是种数据结构&#xff0c;它可以提供快速的插入操作和查找操作。第一次接触哈希表时&#xff0c;它的优点多得让人难以置信。不论哈希表中有多少数据&#xff0c;插入和删除(有时包括侧除)只需要接近常量的时间即0(1)的时间级。实际上&#xff0c;这只需要几条机器指令。…

庖丁解牛|图解 MySQL 8.0 优化器查询转换篇

简介&#xff1a; 本篇介绍子查询、分析表和JOIN的复杂转换过程 一 背景和架构 在《庖丁解牛-图解MySQL 8.0优化器查询解析篇》一文中我们重点介绍了MySQL最新版本8.0.25关于SQL基本元素表、列、函数、聚合、分组、排序等元素的解析、设置和转换过程&#xff0c;本篇我们继续…

Java 底层知识:什么是 “桥接方法” ?

作者 | 小志来源 | 程序员小灰导语笔者在最近的日常工作中&#xff0c;因业务需要&#xff0c;研究 Java 字节码层面的知识。具体是&#xff0c;需要根据类字节码&#xff0c;获取特定方法名的方法入参&#xff0c;此方法名在源码中只有一个。但是在实际使用中发现&#xff1a;…

ACMMM2021|在多模态训练中融入“知识+图谱”:方法及电商应用实践

简介&#xff1a; 随着人工智能技术的不断发展&#xff0c;知识图谱作为人工智能领域的知识支柱&#xff0c;以其强大的知识表示和推理能力受到学术界和产业界的广泛关注。近年来&#xff0c;知识图谱在语义搜索、问答、知识管理等领域得到了广泛的应用。 作者 | 朱渝珊 来源 |…

带你体验云原生场景下 Serverless 应用编程模型

简介&#xff1a; 阿里云 Knative 基于 ASK 之上&#xff0c;在完全兼容社区 Knaitve 的同时对 FC、ECI 工作负载进行统一应用编排&#xff0c;支持事件驱动、自动弹性&#xff0c;为您提供统一的 Serverless 应用编程模型。 背景 阿里云 Serverless Kubernetes&#xff08;A…