欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

一个超时功能的设计

程序员文章站 2022-06-27 21:10:05
超时功能的设计产品需求有一个产品需求,需要执行某个动作之后,需要生成一个超时的任务,在超时时间到了之后执行后续的动作,后续动作的执行大约耗时1秒钟。任务允许在未到超时间删除,超时时间不超过30天。要求在现有的产品架构上实现此功能。产品架构分布式服务实例数量:3缓存:redis 3.2.3数据库:mysql 5.7消息队列:rabbitMq 3.7.6分布式定时任务:elastic-job 2.1.5功能设计方案一首先看到这个需求,第一时间想到的就是在内存中维护一个集合,然后通...

超时功能的设计

产品需求

有一个产品需求,需要执行某个动作之后,需要生成一个超时的任务,在超时时间到了之后执行后续的动作,后续动作的执行大约耗时1秒钟。
任务允许在未到超时间删除,超时时间不超过30天。要求在现有的产品架构上实现此功能。

产品架构

  • 分布式服务实例数量:3
  • 缓存:redis 3.2.3
  • 数据库:mysql 5.7
  • 消息队列:rabbitMq 3.7.6
  • 分布式定时任务:elastic-job 2.1.5

功能设计

  • 方案一
    首先看到这个需求,第一时间想到的就是在内存中维护一个集合,然后通过定时任务每分钟扫描集合,将到过期时间的任务取出并执行后续的操作。
    删除任务则直接将集合中的任务删除。
    一个超时功能的设计

    • 优点:
      • 代码逻辑实现简单。
      • 充分利用了所有实例,每个实例指需要负责自己内存中的任务即可。
    • 存在问题:
      • 每次全量扫描集合过于浪费cpu性能,比如许多任务远还未到过期时间的任务。优化方案:采用有序集合,每次只需要扫描第一个元素,如果
        第一个元素还未到过期时间直接等待下次循环。
      • 任务未持久化,假如服务器重启,宕机或者服务器不可恢复会丢失任务。由于任务时间存在一个月,因此这点算是比较致命的问题。
  • 方案二
    既然方案一存在持久化的问题,那么只要解决这个问题即可,比如存储在一个公共的存储上面,如mysql和开启持久化功能的redis。那么到底应该选择什么
    来存储呢。mysql和redis都是天然支持有序的记录。只是两者使用的数据结构不同,mysql采用b+树,而redis采用的是跳表。对于查询而言,两者的时间
    复杂度都为n(logn)(redis直接在内存中相比mysql而言还少了io)。但是由于我们过期时间插入是非顺序的,对于mysql索引插入和删除同时还会带来页的分裂,而跳表插入和删除
    更加简单。综合考虑这里采用redis存储。
    一个超时功能的设计

    • 优点:
      • 解决了持久化问题,按照不同服务实例id存储不同的zset。
      • redis不仅拥有更优秀的查询效率,还有更优秀的插入和删除效率。
    • 存在问题:
      • 虽然解决了持久化问题,但是由于不同实例的zset是不同的,如果某个实例挂了无法启动,那么其过期任务在超过了过期时间之后,依然不会执行。
        那么是否可以将所有任务都丢到同一个key中而不区分实例呢。确实是可以的,这么做的话对于服务和存储来说都是无状态的,服务只要消费同一个zset
        中的数据即可。
  • 方案三
    所有添加的超时任务都存储到同一个zset中,所有实例的超时线程去消费这个zset。但是这就带来了一个问题,由于之前不同实例是不同zset因此不需要去分配
    具体消费什么任务,自己的zset中有什么任务到期就消费什么任务。现在所有的任务都在一个zset中应该如何去分配呢,获取到期任务的指令是zrange,只能获取sorce范围内的所有
    数据,如果每个实例都是这么获取任务,那么必然都是在重复消费。在redis5.0之后的版本中存在zpopmax/zpopmin命令可以满足这个需求,每次实例线程取任务时弹出第一个任务。但是在这之前的版本并不支持。
    一个超时功能的设计

    • 优点
      • 解决了服务实例挂了无法启动,这个服务实例分配的任务无法执行问题。
    • 存在问题
      • 多实例间任务无法分配。如果只使用一个实例消费过于浪费性能,同时无法扩容。(将数据存储在数据库中也会带来同样问题,同时还会带来并发消费问题)
  • 方案四
    由于方案三存在任务分配问题,那么是否可以单独做一个线程来分配所有已经到期的任务呢。因此就单独使用一个线程来获取所有已经到期的任务,然后在将这些任务均匀的
    分配到所有实例线程中去。最好的方式就是采用mq了。将该线程从redis中获取的所有已经到期的任务都丢到队列中,所有实例线程都消费队列即可,不但解决了分配问题,
    同时也不存在并发问题,任务丢失等问题。

    • 具体实现:
      分配任务线程可采用elastic-job,只使用一个分片,不但避免了获取任务并发问题,同时elastic-job可以保证高可用,当分配任务线程实例挂了之后,自动
      寻找新的实例启动分配线程。分配线程每分钟去redis中获取一次已经到期的任务,同时将任务发送至mq中,同时每个实例中都存在监听这个队列的消费者,来完成后续
      的操作。
      一个超时功能的设计
    • 优点
      • 解决了所有问题。
    • 缺点
      • 实现复杂,引入中间件和组件过多。
  • 方案五
    是否有更简单的方法来实现这个功能呢,答案是有的。既然使用了mq,可以在任务生成的时候直接发送到mq中,利用mq延迟队列的属性来直接完成这个功能。当队列中的消息
    到了过期时间之后才会被消费者消费。如rabbitmq可以采用ttl和死信队列来完成这个功能。
    一个超时功能的设计

    • 优点
      • 解决了所有问题,同时实现简单。
    • 缺点
      • 已经添加的任务无法删除,必须要能够在执行之前可以判断此任务是否被删除。

总结

其实这个是在研发时候经常会遇到的需求,在不同的需求下有不同的解决方案。以上是自己在遇到这个需求时候的考虑,最终选择的是方案五。但是可能存在更好的解决方式。
再此也是记录一下这个看似简单的需求,背后自己的思考。如果有更好的解决方案,也希望大家不吝赐教。

本文地址:https://blog.csdn.net/jy00733505/article/details/107372978

相关标签: 架构设计 队列