系统设计-领导让我设计一个任务系统

点击上方名片关注我,为你带来更多踩坑案例

5b759c05c0e819d1cf81a92cc0da9a7e.png

- 引言 -

    如果你是一个摸爬滚打几年的开发者,那么这个阶段,对系统设计的合理性绝对是衡量一个人水平的重要标准。

    一个好的设计不光能让你工作中避免很多麻烦,还能为你面试的时候增加很多谈资

    而且,不同设计之间理念都是有借鉴性参考性的,你见过的设计多了,思考的多了,再次面临一个问题的时候,就会有很多点子不由自主的冒出来。

    希望这个系列的文章,能够和大家互相借鉴参考,共同进步。

- 需求 -

    需要一个文本内容拆解系统,来按照不同规则拆解各个业务端上传的文件,并把拆解结果返回给业务端。

- 分析 -

    需求听着很简单,这个业务系统做的事情其实就是接任务、做任务、返回结果三件事。

    考虑到是个B端系统,虽然业务端数量不少,但都是内部服务,流量可控,但是领导说将来存在接入C端的可能性,所以多少还是要考虑下并发拓展性的。

    所以接任务阶段不用http接口,选用消息队列,这样后面流量起来了,也可以做控制。

    但是万一任务多了消息挤压在rocketMq怎么办?虽然它的消息不会丢失,但是为了安全性和可维护性,便于排查,所以消息的消费主要是任务持久化到任务系统,之后再异步开始任务,这样可以快速处理消息。

    很显然,主要耗时其实在任务的拆解过程,也就是任务开始后的处理过程,但是这个时候作为消息队列客户端,消息已经消费成功返回了,就不用考虑用rocketMq来做流量控制了。

    那这个时候不考虑流量控制会发生什么呢?

    很显然,如果任务请求很多的情况下,任务存储是很快的,这个时候就会有大量的任务处在任务处理阶段,很快会把异步线程池打满,之后的任务只能采取线程池的拒绝策略。

    但这显然不是一个好的办法,现有的拒绝策略都不合适,重写一个拒绝策略的话,在线程池已满的情况下,也没什么好的办法。

    所以我们必须在内存中做一个流量控制,也就是说做一个内存任务队列来控制,且要考虑到超出流量的任务,之后还能够再次尝试入队开始任务。

    基于以上思考,最后设计的流程图如下

05c7f6e319548239acaa6ff65f1b4fe4.png

下面根据流程图来解析几个关键步骤

- 任务存储 -

    这一步其实是比较简单的

    任务系统实例作为消费者,需要执行的操作就是任务的持久化,之后就返回ack,整个过程可能在100ms以内,随即标志消息消费完成,可以消费接下来的消息。

    按照当前的流量,暂时打算搞两个任务系统实例,每个实例的消费者线程池核心线程数为20,所以rocketMq的并发queue设置为40。

    这样每秒持久化几百个任务是没什么压力的,足够应对短期内的需求

- 内存队列 -

    搞内存队列的目的

  1.  可拓展任务类型,比如异步线程池容量是50,我可以给拆解任务队列20个,剩余30个分配给其他类型的任务,虽然目前它只是用来做拆解的。

  2. 可以做一个优先级排队,比如每次任务启动的时候都取数据库中等待状态的且优先级最高的一个任务入队。

    总之就是尽量别把线程池打满,利用拒绝策略去做一些事情

    具体实现可以使用LinkedBlockingQueue+单例模式去做,也可以使用别的方式。

- 任务入队 -

    比如我目前队列的大小为5,每个任务执行时间为10s

    每次任务存储完成,启动任务的时候,都会去从库中查询status=wait且优先级最高的任务来尝试入队启动。

    10s内前5个任务都可以顺利启动,一直到第6个任务进来,发现队列已满,会把它置为wait状态。

    队列中的任何一个任务完成之后,会触发一次任务启动,来启动一个此时等待中的优先级最高的任务。

- 任务执行 -

    这个过程其实就和具体业务挂钩了

    任务执行流程简单的话,可以一个从开始执行到结束返回都用一个流程,任务复杂一些,比如还要等待异步的返回结果,此时需要将任务挂起。

    上述流程图可以改为

5b96333bc25a664c97e82bba28f7a511.png

    任务挂起的时候,出队但是并不通知业务端,因为此时任务还未完成,一直等到三方接口返回结果后,再处理这个任务。

    处理的过程可以使用内存队列+提高挂起任务优先级的方式,也可以不使用,因为它用的大概率并不是同一个线程池,这个具体情况具体分析吧。

- 任务结束 -

    任务结束,自然就是出队且发送消息给业务端。

    返回消息的时候,可以做一个优化,就是不同业务端打不同的tag,业务端根据tag去监听处理,这也是rocketMq的一个好处哈哈。

- 任务进度状态回调 -

    如果业务端需要回调任务执行状态,比如百分之多少之类的。

    可以在入参中加入参数控制,然后执行任务的途中发送消息给业务端。

- 停机补偿 -

    既然是内存队列,正常情况下是可以形成一个完美的封闭循环的。

    但是如果系统重启了,就会出现一些问题。

    比如重启之后没有新任务进来,那么之前等待的任务谁来唤醒?

    这个时候就要加一个启动补偿机制

@Bean
    public void taskRecover() {
        int capacity = TaskQueueManager.getInstance().getCapacity();
        // 尝试启动次数为队列大小,防止线程饥饿
        for (int i = 0; i < capacity; i++) {
            // 任务启动
            eventService.fireAsynchronous(EventConstants.EVENT_START_TASK);
        }
    }

当然,这只是解决了一个问题,还有很多问题需要解决,会放在另一篇文章详细解释,大家可以关注我。

- 结束语 -

    大概流程就是这样了

    还有一些更细节的东西,比如任务中断了怎么办?下次执行的时候从头执行么?执行过程是否以状态驱动还是?甚至更偏代码层的一些细节就不贴了,本文主要讲的是设计思想,不注重实现,需要代码或者有疑问的同学可以留言,我看到的话会回复的。

    如果你看到这里,感谢

猜你喜欢

转载自blog.csdn.net/qq_31363843/article/details/128027179