中小企业任务系统设计

背景:

设计一款任务调度系统, 客户端定时向服务器获取并完成任务。

关键: 被动下发

表设计:

任务表:

CREATE TABLE IF NOT EXISTS `task_params_template` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `create_time` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00',
      `status` int(11) NOT NULL DEFAULT 0,
      `params` JSON NOT NULL comment '参数模板 例如: 告知任务有A,B,C参数',
      PRIMARY KEY (`id`)
    ) ENGINE=Innodb DEFAULT CHARSET=utf8mb4 comment '任务参数模板表';


CREATE TABLE IF NOT EXISTS `task_once` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `type` int(11) NOT NULL DEFAULT 0 comment '0: 单次任务 ',
 `executor` varchar(256) NOT NULL DEFAULT '' comment '' 执行群体,可以是组或者具体uid. uid_xxx,gid_xxx",
 `start_time` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' comment '单次任务时设定',
 `params_template_id` int(11) NOT NULL DEFAULT 0,
 `params` JSON NOT NULL comment '派发参数,根据参数模板给出字段进行真实值填写',

 `status` int(11) NOT NULL DEFAULT 0 comment '1: 有效 2:删除',
  PRIMARY KEY (`id`),
  KEY `start_time` (`executor`, `start_time`)  
 ) ENGINE=Innodb DEFAULT CHARSET=utf8mb4
注意: 索引建立思考同下。 这里可能对清除表内容有难点,通常需要清除几年前的单次任务之类.

CREATE TABLE IF NOT EXISTS `task_rotate` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `type` int(11) NOT NULL DEFAULT 0 comment '1:间隔定时任务',
 `space` int not null default 0 comment '相隔分钟数',
 `executor` varchar(256) NOT NULL DEFAULT '' comment '' 执行群体,可以是组或者具体uid. uid_xxx,gid_xxx",
 `start_time` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' comment '隔天任务起始时间',
 `daily_time` TIME NOT NULL DEFAULT '00:00:00' comment '隔天任务任务任务时间',
 `params_template_id` int(11) NOT NULL DEFAULT 0,
 `params` JSON NOT NULL comment '派发参数',
 `status` int(11) NOT NULL DEFAULT 0 comment '1: 有效 2:删除',
  PRIMARY KEY (`id`),
  KEY `start_time` (`executor`, `daily_time`,)    
 ) ENGINE=Innodb DEFAULT CHARSET=utf8mb4
注意: 这里会想,区分度大的dailly_time为什么不放前面。因为通常执行操作为where executor=xxx and dailly_time>=xxx , 区分度大的时间是范围搜索,则不适合做前者。 同时executor 进行hash 分区。 

约定事项:

任务表公共控制参数:

task_dispatch_times: 任务下发次数. (不设置则默认下发一次,用于多次数执行任务) 



参数功能:

exclude_task_id_list: 排除的任务id列表,避免相同时间任务重复下发(这里每个任务表需要有一个)

功能:

定期执行任务: 用于下发每几分钟,每天或者每几天需要做的任务

多次数执行任务: 用于下发于执行群体供多个用户执行的任务。 例如: 下发多次执行6次的任务给执行群体,该群体有20人,则里面只有6个人能竞争拿到该任务,拿完即止。同时,当拿到任务的人做完后及时任务还有下发次数,不会继续拿

多次数阻塞执行任务: 同多次数执行任务,但是 当拿到任务的人做完该任务后发现还有下发次数时,会继续拿该任务直到拿完。


任务表常用操作:

一.用户拿任务,必须在上次任务完成回调通知后才能取下一个任务:(这里意味着,同一个用户同时只有一次请求,不需要对单用户加锁)
参数: uid 用户id

  1. 获取任务时间,任务日志号: 服务器从缓存获取用户上次已完成任务,key:last_completed_task_{uid} ,通过任务获取上次已完成任务时间为LAST_START_TIME,若无该任务则把相对当前时间n小时前作为LAST_START_TIME。 设LAST_START_TIME n小时之后时间为 CACHE_TIME(n<24) , 设LAST_START_TIME的时分秒部分为 LAST_START_TIME_HMS

  2. 查看 最近派发任务 key: recent_task_{uid} 中{recent_task}的 任务日志号 与上一步获取的 上次已完成任务的 任务日志号是否相同,若不相同则返回该任务进行任务重做。若相同,则进行下一步。

  3. 获取用户所属群体GID 和 用户id UID

  4. 从缓存换取群体GID相关信息缓存对象(创建时间) 记录为 task_info_{GID},无合法则创建有则合法使用:
    判断是否合法:

    1. 存在

    若不合法:

    1. 设置创建时间为当前时间
    2. 放入缓存(这里可能会出现雪崩,万一大量同时进入这个创建的逻辑, 则后者创建会覆盖前者,所以需要避免多人同时创建。这里可能有办法: 发送给单独服务统一处理,加锁等)
    3. 写入缓存 key: task_info_{GID} value: task_info ttl: 自定义吧,几个小时或者几天
  5. 从缓存获取任务优先级队列,无合法则创建有合法则使用.
    判断是否合法,同时满足下面条件:

    1. task_list_struct存在,且过期绝对时间小于当前时间
    2. 创建时间大于 task_info_{GID} 的创建时间

    若不合法,则:
    构建任务优先级队列结构, task_list_struct(最小堆结构任务对队列, 创建时间, 过期绝对时间 ):

    1. 获取单次任务队列: select * from task_once where start_time>=LAST_START_TIME
      and (executor=GID or executor =UID)
      and status=1
      and start_time < CACHE_TIME
      and id not in {exclude_id_task_list} , 取出时间范围内的单次任务记录为 once_task

    2. 初步构建优先级队列: 获取单次任务列表构建最小堆task_list (O(n) 这个具体需要考证)

    3. 获取轮转任务列表,并且插入到最小堆:

      1. 选出所有轮转任务(可能有更优): select * from task_rotate where 1=1
        and start_time < LAST_START_TIME //判断任务是否达到初始时间
        and (executor=GID or executor =UID) // 属于执行者
        and status=1 记录为rotate_task
      2. 遍历每个轮转任务, 为每个轮转任务生成单次任务,直到任务执行时间大于等于CACHE_TIME. 其中第一个执行时间next_start_time= start_time + ( [ (LAST_START_TIME- start_time)/space ] +1 ) * space , 并且全部插入至最小堆.

      注意: 这里的设计取出了该执行者所有轮转任务,只用到了执行者一个索引, 所以这里管理端需要限制轮转任务数量,例如每个组轮转任务不能超过100个同时对单个用户下发的不能超过100之类(对于这里的设计其实想过不少也不是很能比较完美,有提议欢迎评论)

    4. 设置task_list_struct过期绝对时间为CACHE_TIME

  6. 从上一步获取的优先级队列获取此次该做任务(这里有口库存问题). 获取出任务最小堆第一个,对task_dispatch_times 参数减一,相减成功则事务去 扣减数据库中的此变量,数据库中相减后结果若小于等于0,则pop出堆并重新调整最小堆,否则只更新task_dispatch_times 参数。然后放入缓存

    • key: task_list_struct_{uid}
    • value: {task_list_struct}
    • ttl: task_list_struct结构里的过期绝对时间
  7. 生成任务日志: 生成任务日志号, 异步发送任务下发日志(日志号id, 用户id, 任务id,派发参数,日志类型:派发)至kafka队列

  8. 生成排除id队列:exclude_task_id_list 等于上次已完成任务的exclude_task_id_list加上该次取出任务的id

  9. 记录该用户的最近派发任务于缓存: key: recent_task_{uid} value: recent_task(派发参数,派发次数,排除队列exclude_task_id_list)

  10. 返回task任务(任务派发参数, 日志号id )

二.用户任务完成,回调告知任务完成
参数: 用户id,日志号id,完成结果

  1. 获取 用户最近派发任务 redis key: recent_task_{uid} , 判断其任务日志号id是否与参数中的相等,相等着继续以下步骤.
  2. 最近派发任务写入缓存更新该uid上次已完成任务: key:last_completed_task_{uid} value: {recent_task}(任务时间,派发参数,日志id, 排除队列exclude_task_id_list)
  3. 异步写任务完成日志(日志号id, 用户id, 任务id,派发参数,日志类型:完成)进kafka

三.任务日志处理

  1. 批量消费至es,实时分析

  2. 若数据量太大需要离线分析则需要另起一份消费至hive,做离线分析

    注意: 可以使用 批量消费至mysql,统一使用(insert into xx on duplicate xxx) 作为前期处理.

四.管理页面:

1.展示某个时间开始的任务列表或者某个组,用户时间开始的任务列表:
通常默认查询7天内数据指定默认起始时间START_TIME
查询执行者xx在某段时间开始的任务列表:(同获取任务方法,这里属于管理后台处理,其实并发量不大)

2.对任务增删查改:
若更改执行对象是群体的:

  1. 更新数据库.
  2. 清除群体GID相关信息缓存对象: redis del 执行对象task_info_{GID}
  3. 返回成功

若更改对象是个体对象:

  1. 更新数据库.
  2. 清除个体优先级队列缓存: redis del 执行对象task_list_struct_{uid}
  3. 返回成功

注意:

  1. 这里操作更新数据库和更新缓存,其实涉及分布式事务。 若不考虑一致性,在更新数据库后清除缓存宕机了,该缓存队列过期时间到了自动就会刷新。若需要较快恢复性,则考虑使用队列分布式事务来实现.
  2. 这里删除缓存来实现,而不是直接重新构建每个执行个体的优先级队列,这样能时缓存失效重建错峰进行且只建立有来请求的有效个体.
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页