在项目中使用到分布式任务框架,用起来比较好用实用,便研究了一下设计的原理。由于是公司自研的分布式框架,不便写的太过详细,这里主要记录一下设计的原理。同时我也会对比我以前使用的任务框架,做一下对比
公司自研的分布式任务对客户端来说,使用感觉友好,代码基本没有的业务入侵。任务框架也使用了一些第三方的中间件,主要使用了如下组件以及作用
- 数据库(Mysql):保存用户的定时任务配置数据,如corn表达式等
- 消息队列(RabbitMQ):当任务触发时,任务框架的服务端发消息,触发客户端执行任务
- Zookeeper:用来为任务提供分布式锁,保证任务只会在一个实例上执行
这里对客户端还是有一定的侵入性的,首先用户的定时任务配置数据,是保存在客户端的,需要每个客户端实例使用的数据库中都要添加一个Task表。当用户需要添加Task时,需要在客户端自己库的Task表中插入任务配置数据,这些配置数据也会同步给服务端(这里就涉及到数据一致性的问题,后面会讲)
这与我以前在平安使用的任务中间件不同,平安的任务框架,任务配置数据是保存在服务端的,服务端会提供一个web页面来给使用Task的服务添加任务,配置任务等,当然也支持,在web上面对任务做权限控制,即时触发等。而我这里使用的任务数据配置在客户端的Task表中,权限控制是没办法做的
Task任务表设计如下,其实就是一些定时任务常见的配置项,corn表达式,任务类型,触发时间之类的。
-- st_trading.Task definition
CREATE TABLE `Task` (
`id` varchar(256) COLLATE utf8mb4_general_ci NOT NULL,
`type` varchar(256) COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务标识',
`firstTriggerTime` datetime NOT NULL COMMENT '首次触发时间',
`cronString` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'cron表达式,为空标识是一次性task',
`dataMap` text COLLATE utf8mb4_general_ci COMMENT '任务参数',
`nextTriggerTime` datetime NOT NULL COMMENT '下次执行时间',
`round` int NOT NULL,
`createTime` datetime NOT NULL,
`timeZoneId` varchar(30) COLLATE utf8mb4_general_ci DEFAULT 'GMT' COMMENT '任务执行时区',
`mutexId` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分布式锁key',
`canaryVersion` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '灰度版本',
`contextMap` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL,
`status` int DEFAULT '0',
`priority` int NOT NULL DEFAULT '2' COMMENT '优先级',
PRIMARY KEY (`id`),
KEY `IX_Task_nextTriggerTime` (`nextTriggerTime`,`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
这个表在每个客户端都会创建一个,服务端也会有这个表,保存全部客户端的定时任务配置数据。整体架构如下图
举个例子,假设Task表有如下数据
当客户端创建了一个名为target_clear_task_us
的Task时,在MQ中也会创建名字一样的topic,当定时任务触发时,任务的服务端会给target_clear_task_us的topic下发任务触发的消息,客户端收到消息会就会执行任务。
但是这里有一个问题,没办法保证任务名称的唯一性,假如全公司很多团队都是使用这个任务框架,那么task的名称就有可能会一样,这时任务是没办法触发的,只能与对应的开发沟通修改任务名称。
前面说到服务端会保存所有客户端的任务配置数据,从这个图中可以看到,服务端同步客户端数据的方式,是简单粗暴的连接了所有客户端的数据库,然后每10秒轮询所有的任务数据,同时也判断任务是否要触发。这里我曾经思考过,是否可以改成监听客户端binlog的方式同步数据,而不是轮询。但是后面想了想轮询的做法有几个好处
- 可以保证服务端数据与客户端数据的一致性,因为服务端不保存数据,任务配置数据全部是由客户端维护的
- 可靠性,如果是binlog的方式,意味着要增加一个中间件的使用,降低了可靠性,提高了维护成本
- 因为是服务端代码实现了,可以做一些加入一些异常告警,权限控制等
当然也有缺点:
- 如果有新的服务接入Task,需要把数据地址告知服务端,也需要发布版本。如果是binlog同步的话,服务端完全是无感的
- 对于服务端同样有一定的侵入性,因为需要维护所有客户端的数据库连接