适用场景:
延时任务有别于定时任务,定时任务往往是固定周期的,有明确的触发时间。而延时任务一般没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件。也就是说,任务事件生成时并不想让消费者立即拿到,而是延迟一定时间后才接收到该事件进行消费。定时任务首选 Medusa CronJob , 有DAG需求或者大数据任务的选Airflow
- 在订单系统中,一个用户某个时刻下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将自动进行过期处理;
- 车后养护订单下单后会锁库存,7天后用户还不来店就需要释放库存;
- 车后订单,15分钟后提醒店家下单。
一、技术架构
延时队列架构如上图所示,重要组件说明如下:
- gRPC server:获取client发送过来的请求,根据请求(request)中的触发时间(triggerAt)与设置的最大延时时间(max_delay)进行比较,把超过max_delay的request中的延时任务信息(task)中的(triggerAt, taskId)存储到redis的有序集合(SortedSet)"xn_delay_wait_queue"中。把没有超过max_delay的task(triggerAt, taskId)存储到redis的有序集合"xn_delay_queue"中,同时把task详细信息写入到redis的hash中,key值为taskId。把task信息以及request_id等信息发送至kafka的"kafka.default.topic"中,后续持久化到MySQL中;
- consumer(runloop):获取"kafka.default.topic"中的消息,将task信息写入MySQL的task表中,或者将task的状态信息更新至task表中对应的task中;
- fetcher(runloop):获取redis中有序集合"xn_delay_wait_queue"中的满足条件的task,根据这些task的taskId获取MySQL task表中的task详情,并把这些task详情写入至redis的hash中,key为taskId,同时把task(triggerAt, taskId)写入至有序集合"xn_delay_queue"中;
- worker(runloop):获取redis中有序集合"xn_delay_queue"中的满足条件的task,根据taskId获取redis hash中的task详情,然后执行这些task对应的任务(kafka任务、gRPC任务、HTTP任务);
- redis:有序集合存储延时队列中的任务、等待进入延时队列的任务。hash存储延时队列中的task的详情信息,使用完成后删除task详情;
- mysql:持久化所有task信息及request_id。
二、详细说明
2.1 创建延时任务
通过gRPC或者HTTP方式创建延时任务,延时队列的处理原理相同,因此下面章节不进行单独描述。
2.1.1 创建gRPC延时任务
1)发送gRPC延时请求至gRPC server
client发送request到gRPC server,调用CreateGrpcTask(),其中request信息如下表所示:
字段名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
trigger_at | int64 | Y | 触发时间(时间戳毫秒值) |
service | string | Y | 请求的serviceName或packageName.serviceName |
retry | object | N | RetryInfo,任务运行失败后最大重试次数count和重试间隔interval(秒) |
project | string | Y | 项目名 |
method | string | Y | grpc请求的methodName |
host | string | Y | 任务请求的grpc服务host |
group | string | Y | 组名 |
format | int | Y | GrpcBodyFromat |
body | bytes | Y | 根据Format指定编码器编码后的二进制数据 |
gRPC server将接收到的request进行处理,生成task_info,包括task_id、group、project、trigger_at、retry、payload(host、service、method、body、format),对trigger_at与配置的max_delay进行比较,若trigger_at大于max_delay,则把task_id、trigger_at加入到延时等待队列中(redis的有序集合"xn_delay_wait_queue");若trigger_at小于max_delay,则把task_id、trigger_at加入到延时队列中(redis的有序集合"xn_delay_queue"),同时将taskinfo信息写入到redis hash中,key为task_id,field为basic_info(group、project、trigger_at、retry、type)进行encode后的值,value为payload(host、service、method、body、format)进行encode后的值。xn_delay_queue中的task与hash中的task一一对应。
gRPC server将task写入redis的示意图如下所示:
然后gRPC server将task_info发送至kafka的"kafka_default_topic",用于后续的持久化。
2)consumer从kafka topic消费消息
consumer从kafka topic中获取task_info,然后对task_info中的payload信息进行decode操作(与上面encode对应),获取task_info后将其写入至MySQL task表中,column["task_id"|"task_type"|"group_name"|"project"|"payload"|"trigger_at"|"retry_count"|"retry_interval"|"task_status"|"created_at"|]。
3)fetcher将xn_delay_wait_queue中满足条件的加入至xn_delay_queue
fetcher不断的轮询xn_delay_wait_queue,找到满足trigger_at<server.wait_delay的task,根据这些task的task_id获取MySQL task表中的task_info信息,然后将task_info写入到redis hash中,key为task_id,field为basic_info(group、project、trigger_at、retry、type)进行encode后的值,value为payload(host、service、method、body、format)进行encode后的值。接着,把task_id、trigger_at加入到延时队列中(redis的有序集合"xn_delay_queue"),同时删除延时等待队列中(redis的有序集合"xn_delay_wait_queue")的task_id对应的记录。
fetcher将xn_delay_wait_queue满足trigger_at<server.wait_delay的task加入值xn_delay_queue的示意图如下所示,
4)worker执行xn_delay_queue中满足触发条件的task
worker不断的轮询xn_wait_queue,找到满足now大于等于trigger_at的task,拿到task_id后删除延时队列中(redis的有序集合"xn_delay_queue"的task_id对应的记录。根据task_id获取redis hash中对应的field[basic_info(group、project、trigger_at、retry、type)],value[payload(host、service、method、body、format)]进行decode操作,然后根据type类型执行对应的任务(kafka任务、http任务、gRPC任务),执行完成后删除redis hash中task_id对应的记录,同时更新MySQL task表中task_id对应的状态为task执行成功。
注:若decode操作或者任务执行失败,worker将会删除redis hash中task_id对应的记录,同时更新MySQL task表中task_id对应的状态为task执行失败,以及更新retry次数为当前retry次数。若retry次数小于等于request中的设置的retry次数时,将task_id,trigger_at加入至延时队列中(redis的有序集合"xn_delay_queue"),然后将task_info写入到redis hash中;若retry次数大于request中的设置的retry次数时,则删除redis hash中task_id对应的记录。
worker执行示意图如下所示
2.1.2 创建HTTP延时任务
原理与创建gRPC延时任务一致,仅task类型不一致,为HTTP任务,后续具体执行为HTTP任务,这里不再赘述。
2.1.3 创建kafka延时任务
原理与创建gRPC延时任务一致,仅task类型不一致,为kafka任务,后续具体执行为kafka任务,这里不再赘述。
2.2 取消延时任务
用户通过gRPC或者http方式发送取消延时任务请求后,gRPC server根据task_id执行删除redis中hash中对应的task_info,以及redis的有序集合"xn_delay_queue"或者"xn_delay_wait_queue"中的task信息(task_id,trigger_at),同时更新MySQL task表中task_id对应的状态为task取消。
2.3 查询延时任务状态
用户通过gRPC或者http方式发送取消延时任务请求后,gRPC server根据task_id查询MySQL task表中task_id对应的状态为task信息,返回task的状态信息(task_type, trigger_at, task_status)。