一、背景
在日常编程中,总是会遇到延时执行的任务。比如:定期发送邮件,定时上架商品;再比如订单在一定时间内未支付,需要到期关闭订单。
你也许会借助分布式任务xxl-job来实现,不仅性能差,cron执行的间隔时长也影响了业务的准确性。
本文及下面的几个文章,将讨论如何实现一个通用的延时任务通知服务:
- 设计方案
- redisson的延迟队列RDelayedQueue
- JDK的延迟队列DelayQueue
- webhook执行任务
二、业务要求
- 支持业务方对任务进行增删改查
- 支持跨服务之间的调用
- 延时任务通知服务,作为基础服务,不关心具体业务
三、技术要求
- 回调时间的误差在2秒内
- 数据需要持久化
- 系统扩展性强:当任务量大的时候,可以横向扩展节点
四、设计方案
1、任务模块
业务服务通过http接口,对任务进行增删改改, 实时同步mysql数据库。
使用分布式缓存redis保存任务,提高任务的读取效率,redis 的key是任务的交易流水号。
作为任务的唯一标识,业务方需要生成一个交易流水号,保证全局唯一。
2、延迟队列模块
这是本文的重点,这里介绍两种实现。
- redisson RDelayedQueue (分布式延迟队列)
- Java DelayQueue (进程内的延迟队列)
后者是进程内的延迟队列,需要我们去解决分布式问题,相对会复杂一些。
任务模块会调用延迟队列模块,当任务被增删改的时候,需要同步至延迟队列。
3、webhook模块
定义一个延迟队列的消费者,监听到期的延迟任务。
得到延迟任务中的交易流水号,查询任务的回调地址和回调内容,执行http回调业务方。
五、数模设计
数据存储只需要一张表 ---- 任务表,见下:
CREATE TABLE `notify_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_code` varchar(64) NOT NULL COMMENT '任务编号',
`trans_no` varchar(32) NOT NULL COMMENT '交易流水号',
`notify_date` datetime NOT NULL COMMENT '通知时间',
`notify_url` varchar(255) NOT NULL COMMENT '通知地址',
`notify_params` varchar(4000) DEFAULT NULL COMMENT '通知内容,json格式',
`is_retry` tinyint(1) DEFAULT 0 COMMENT '是否支持重试',
`retry_times` int(11) DEFAULT 0 COMMENT '重试次数',
`finished_gmt` datetime DEFAULT NULL COMMENT '完成时间',
`is_finished` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否完成',
`marked` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已标记',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_gmt` datetime DEFAULT current_timestamp() COMMENT '创建时间',
`modified_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`modified_gmt` datetime DEFAULT current_timestamp() COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `UNQ_TRANS_NO` (`trans_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
六、生成任务的交易流水号
上面任务表的交易流水号生成规则是:md5(回调时间+回调地址+回调内容)。
要保证交易流水号唯一的方法有很多,但是业务方在创建任务的时候,可能不想或无法存储交易流水号,而在后面又可能修改任务的回调时间,再或者需要提前取消任务。
建议业务方只基于请求的信息进行Md5,把该值作为双方的交易流水号。
七、接口设计
1、新增任务
参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
---|---|---|---|---|---|
request | request | body | true | TaskCreateRequest | TaskCreateRequest |
isRetry | 是否支持重试 | false | boolean | ||
notifyDate | 通知时间,格式:yyyyMMddHHmmss | true | string | ||
notifyParams | 通知内容,json格式 | false | string | ||
notifyUrl | 通知地址 | true | string | ||
taskCode | 任务编号 | true | string | ||
transNo | 交易流水号,由业务方生成并保存。删除和修改任务时,需要传递该值 | true | string |
{
"isRetry": true,
"notifyDate": "20240426163900",
"notifyParams": "{\"id\":2,\"name\":\"Jack\",\"position\":\"Assistant\"}",
"notifyUrl": "https://webhook.site/76b884f7-ecf5-4821-9d98-4042ae776cd3",
"taskCode": "webhooksite",
"transNo": "123456089"
}
2、修改任务
参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
---|---|---|---|---|---|
request | request | body | true | TaskEditRequest | TaskEditRequest |
notifyDate | 通知时间,格式:yyyyMMddHHmmss | true | string |
{
"notifyDate": "20240426163400"
}
3、删除任务
限于篇幅,本文对延时任务通知的设计方案就整理到这里,后面将详细介绍延迟队列的实现。
附:相关系列文章链接
延时任务通知服务的设计及实现(一)-- 设计方案
延时任务通知服务的设计及实现(二)-- redisson的延迟队列RDelayedQueue
延时任务通知服务的设计及实现(三)-- JDK的延迟队列DelayQueue
延时任务通知服务的设计及实现(四)-- webhook执行任务
延时任务通知服务的设计及实现(五)-- Netty时间轮HashedWheelTimer