当前位置: 移动技术网 > IT编程>数据库>Mysql > 一个超时功能的设计

一个超时功能的设计

2020年07月17日  | 移动技术网IT编程  | 我要评论
超时功能的设计产品需求有一个产品需求,需要执行某个动作之后,需要生成一个超时的任务,在超时时间到了之后执行后续的动作,后续动作的执行大约耗时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

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网