在数据库业务场景中,我们常需要基于表的状态变化来触发后续的定时任务,比如当订单表的状态更新为待支付时,自动启动一个30分钟后检查支付状态的定时任务,这种需求可以通过触发器结合表状态的设计来实现。

核心实现思路
整个方案的核心逻辑分为三个部分:首先在业务表中添加状态标识和时间字段,用于记录触发定时任务的条件;然后编写触发器,当表的状态满足预设条件时,将任务信息写入专门的任务调度表;最后通过数据库的定时任务功能扫描调度表,执行对应的任务逻辑。
1. 表结构设计
首先需要准备两类表,一类是业务表,一类是任务调度表。以MySQL为例,业务表可以添加如下字段:
-- 业务表示例:订单表 CREATE TABLE `order_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(64) NOT NULL COMMENT '订单编号', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态:0待支付 1已支付 2已取消', `create_time` datetime NOT NULL COMMENT '创建时间', `trigger_task` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否触发定时任务:0未触发 1已触发', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单信息表';
任务调度表用于存储待执行的定时任务信息:
-- 任务调度表 CREATE TABLE `task_schedule` ( `id` int(11) NOT NULL AUTO_INCREMENT, `task_type` varchar(32) NOT NULL COMMENT '任务类型', `related_id` int(11) NOT NULL COMMENT '关联业务ID', `execute_time` datetime NOT NULL COMMENT '任务执行时间', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '任务状态:0待执行 1已执行 2已取消', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_execute_time_status` (`execute_time`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务调度表';
2. 触发器编写
触发器的作用是监听业务表的状态变化,当满足触发条件时,自动向任务调度表插入任务记录。比如当订单状态更新为待支付,且还未触发过任务时,插入一个30分钟后执行的订单检查任务:
-- MySQL触发器示例:订单状态更新时触发任务
DELIMITER //
CREATE TRIGGER `trg_order_status_change`
AFTER UPDATE ON `order_info`
FOR EACH ROW
BEGIN
-- 当状态更新为待支付,且之前未触发过任务时
IF NEW.status = 0 AND OLD.status != 0 AND NEW.trigger_task = 0 THEN
-- 向任务调度表插入记录,执行时间为当前时间加30分钟
INSERT INTO `task_schedule` (`task_type`, `related_id`, `execute_time`, `status`, `create_time`)
VALUES ('CHECK_ORDER_PAY', NEW.id, DATE_ADD(NOW(), INTERVAL 30 MINUTE), 0, NOW());
-- 更新业务表的触发任务标识,避免重复触发
UPDATE `order_info` SET `trigger_task` = 1 WHERE `id` = NEW.id;
END IF;
END //
DELIMITER ;
这里需要注意,MySQL的触发器不支持直接调用存储过程之外的定时任务创建语句,因此我们通过写入调度表的方式间接实现,这也是跨数据库通用的方案。
3. 定时任务关联
接下来需要创建数据库的定时任务,定期扫描任务调度表中待执行的任务。以MySQL的事件调度器为例:
-- 开启MySQL事件调度器
SET GLOBAL event_scheduler = ON;
-- 创建定时事件,每分钟扫描一次调度表
DELIMITER //
CREATE EVENT `evt_scan_task`
ON SCHEDULE EVERY 1 MINUTE
DO
BEGIN
-- 查询待执行且执行时间已到的任务
DECLARE done INT DEFAULT 0;
DECLARE v_task_id INT;
DECLARE v_task_type VARCHAR(32);
DECLARE v_related_id INT;
DECLARE cur CURSOR FOR SELECT `id`, `task_type`, `related_id` FROM `task_schedule` WHERE `status` = 0 AND `execute_time` <= NOW();
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_task_id, v_task_type, v_related_id;
IF done THEN
LEAVE read_loop;
END IF;
-- 根据任务类型执行对应逻辑
IF v_task_type = 'CHECK_ORDER_PAY' THEN
-- 检查订单支付状态,如果仍未支付则取消订单
UPDATE `order_info` SET `status` = 2 WHERE `id` = v_related_id AND `status` = 0;
END IF;
-- 更新任务状态为已执行
UPDATE `task_schedule` SET `status` = 1 WHERE `id` = v_task_id;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
不同数据库的实现差异
不同数据库的触发器和定时任务语法略有不同,需要注意适配:
- PostgreSQL可以使用
pg_cron扩展实现定时任务,触发器中可以直接调用pg_cron的调度函数 - SQL Server可以使用代理作业作为定时任务,触发器中通过存储过程向作业调度表写入记录
- Oracle可以使用DBMS_SCHEDULER包创建定时任务,触发器中可以调用该包的相关接口
注意事项
在实际使用中需要注意以下几点:
- 触发器逻辑要尽量简单,避免复杂的查询和操作,防止影响业务表的写入性能
- 任务调度表需要添加合适的索引,尤其是执行时间和状态的联合索引,提升扫描效率
- 要处理任务重复执行的问题,比如通过状态字段或者分布式锁保证任务幂等性
- 定时任务的扫描频率需要根据业务需求调整,避免过于频繁消耗数据库资源,或者间隔太长导致任务执行不及时
这种方案的优势在于触发逻辑和业务表变更强绑定,不需要在业务代码中额外编写触发定时任务的代码,减少了业务层和数据库层的耦合,同时可以灵活适配各种基于表状态变化的定时任务场景。