延迟消费的几种实现方案

本文探讨了五种实现自动取消未付款订单的方法,包括定时任务、时间轮、Redis失效通知、延迟消息队列和改进的延迟队列,详细分析了各自的优缺点和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求场景:

      平时下单后未完成付款时,一般情况30分钟后订单仍未支付就会变更为已取消;如果让自己设计一下这个自动取消订单的功能,大家先自己想想如何实现呢?

思路1:定时任务

  • 实现思路:写一个定时任务,以固定的频率遍历任务,将满足条件的任务取出并执行。如果任务量比较多,可以考虑任务分片和单个分片多线程处理;
  • 优缺点:
    • 优点:实现简单;
    • 缺点:
      • 时间精度不足:只能支持固定周期的定时触发,例如每分钟执行一次,这样会存在一分钟(某个任务当前任务执行时未到触发时间,任务执行完毕后刚好到触发执行时间,只能等待下次任务调度时才能执行)甚至更长(单个任务执行时间较长)的时间延迟;
      • 数据库压力大:由于时间精度取决于扫表的频率,为了提高精度,需要增大扫表的频率,但是此时给数据库造成较大的访问压力;
      • 支持的任务规模有限:当任务的规模较大时,在一个调度周期内,存在不能完成一遍对任务的扫描的风险,导致任务不能在规定的时间执行;
    • 适用场景:
      • 小规模,对时间精度无高要求的定时任务场景;

思路2:时间轮

  • 实现思路:
    • 时间轮,顾名思义就是一个类似钟表原理,一个包含n个元素的环形数组,数组的每个元素是一个slot,每一个slot是一个链表(类似hashmap),将每个待执行的任务放到slot中,以固定的时长t(每秒)顺序移动指针,移动到的位置从当前链表中取出待执行的任务,执行当前任务。
    • image.png
    • 任务存储与执行逻辑:
      • 定时任务的执行时间相对于当前时间延迟d时间单位(秒/分钟),任务存储在slot中的位置index=d%(n*t),如果同一个slot遇到冲突的任务,追加到当前slot上的链表(与hashmap类似)。
      • 当延迟时间d大于时间轮一周所能表达的最大时长时,即index=d%(n*t)!=0,此时需要引入轮次的概念(可以理解为套圈),任务存储时轮次r=d/(n*t);
      • 执行到对应的slot时,需要同时判断当前任务的轮次r=0?,如果是0,则立即执行,否则当前slot的轮次r=r-1等待下次调度时执行;
    • 拓展:
      • 当定时任务的延迟时间较大时,为了保证执行时间精度,需要n较大的时间轮,此时会占用较大的内存,但是待执行的任务可能时间分布比较稀疏。综合以上情况,时间轮出现了很多变种,例如kafka的延迟队列使用的分层时间轮,类似钟表的秒、分钟、时的层级关系,感兴趣的同学可以了解一下kafka的延迟队列实现思路。

  • 优缺点:
    • 优点:
      • 简洁、高效。时间轮的逻辑思路理解成本较低,且任务存储在内存中,所有的操作都是在内存中,不依赖外部中间件。
      • 扩展性好,当任务规模较大时,可以通过增加机器,通过请求负载的方式,实现扩容;
    • 缺点:
      • 任务丢失风险,所有任务都是在内存中,重启或宕机时,在时间轮中未执行的任务会丢失;
    • 实现成本高:
      • 当延迟时间较大时,就需要分层时间轮,实现成本高。
      • 如果不能容忍任务丢失,需要自行实现补偿机制,也就会增加实现成本。
  • 适用场景:
    • 任务规模不限制,时间精度要求高,可以容忍一定的任务丢失场景。如果延迟的时间越长,任务出现丢失的可能性越大,机器被重启或出问题的概率会变大;

思路3:Redis失效通知机制

  • 实现思路:redis具备key失效时通知的能力,通过设置key的失效时间,key失效时的消息通知来实现延迟消费;实现方案参考:
  • 优缺点:
    • 实现简单;
    • 无延迟时间限制;
    • 性能高,支持的规模取决于redis的能力;
    • 稳定性,因为redis缓存失效存在懒性删除的场景,并且当cpu负载较高时,redis能否及时做到缓存失效,同时下发失效消息,此处待确定(如果有同学对redis缓存失效与通知机制比较了解,请评论指导一下,感谢);
  • 适用场景:
    • 任意规模、短时间的定时任务;

思路4:延迟消息队列

  • 实现思路:
    • 部分消息队列中间件提供了延时消息的能力,支持了一定时间内的消息延迟消费的能力。可以直接使用消息队列,实现延时执行任务。例如rocketMq/kafka等消息中间件;
  • 优缺点:
    • 延迟时间限制,不同消息中间件支持的延迟消费时间方式不同,例如rocketmq当前支持16个不同的延迟等级,且最大延迟时间是2小时,即不支持超过2个小时的延时消息。
    • 实现方式简单,延时消息同普通消息基本无区别,只需要将执行的逻辑写在消费线程中即可。
    • 性能高,支持的任务规模无限制,rocketmq(最大支持1000qps)、kafka基本满足大部分的任务规模,可以直接使用。如果任务规模超过1000qps此方案酌情考虑使用;
    • 稳定可靠,专业的事情交给专业的人来做,不用担心任务丢失、时效的问题,且执行失败时,MQ天然的重试的能力,尽力保证至少被消费一次。
  • 使用场景:
    • 任意规模、短时间的定时任务。

思路5:改进的延迟队列

  • 实现思路:
    • 使用MQ的来实现定时执行的功能,已经相当完美,但是roketmq支持的延迟时间有上限,不能满足本次的业务诉求。所以,提出一种巧妙的“不断延时”思路:
      • 接收到定时任务时,根据任务定时执行的时间,计算需要延迟的时间;
      • 从rocketmq支持的延时级别从大到小遍历,直到找到找到第一个小于要延迟的级别,发送该延迟级别的延时消息,并将任务要执行的时间存到消息中;
      • 延时消息的消费者,在接收到消息时,再次计算任务要延迟执行的时间:
      • 如果延迟的时间小于等于0,则执行任务;
      • 如果延迟的时间大于0,则再次从rocketmq支持的延时时间从大到小进行遍历,找到第一个小于延迟时间的延时级别,再次发送延迟消息;
      • 重复以上步骤
        • 举个例子,rocketmq支持的延时级别是[2h,1h,30m,20m,10m,9m,8m,7m,6m,5m,4m,3m,2m,1m,30s,10s,5s,1s],要设定一个延迟的时间是23小时15分钟的定时任务:
        • 第一次发现23小时15分钟大于2小时,那么先发送一个延迟2小时的消息;
        • 消费者接收都这消息后,发现需要再次延迟21小时15分钟,则再次发送一个2小时的延迟消息;
        • 以此类推,最终会到一个1小时15分钟的消息,那么此时发送一个1小时的延迟消息;
        • 再次消费到该任务时,需要延迟的时间是15分钟,则再次发送一个延迟10分钟的消息;
        • 再次消费到该任务时,发送一个5分钟的延迟消息;
        • 再次消费到该任务时,发现延迟时间为0,可以直接执行任务。
    • 优缺点:
      • 稳定可靠、支撑规模大;
      • 任意时长的延迟;
      • 实现相对简单;
      • 对MQ有压力。一个任务从开始到最终执行,会生成多个消息,对MQ有一定的压力。
    • 适用场景:
      • 任意规模、任意时间精度、任意时长的定时任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值