首页 / 资讯中心 / 文章详情

MySQL事件调度器实战:用Event Scheduler替代Cron实现定时清理与归档

MySQL事件调度器实战:用Event Scheduler替代Cron实现定时清理与归档 ★ FEATURED ARTICLE
如果你手头有一个业务库里面有一张每天都在膨胀的日志表老板要求保留90天过期数据每天凌晨自动清掉你会怎么做我见过不少团队的第一反应是在服务器上挂Cron写一条mysql命令半夜跑一把。这个方案不是不能跑真跑起来到处是细节——脚本散落在不同机器连接配置要跟着改时区不一致会导致清理时间漂移最要命的是Cron任务失败时并没有人主动通知你往往是第二天数据涨了一大截才发现。后来我把这套定时清理的逻辑全部搬进了MySQL自己的事件调度器Event Scheduler里用MySQL事件来跑才算把这块石头落了地。如果你正好负责中后台数据库的日常维护或者要给某个业务模块加自动化的周期性任务这篇内容比较对你胃口。我会把事件调度器的工作机制、创建方法、常用场景以及我踩过的那些“怎么不执行”“怎么两边重复执行”的真实问题一并讲清楚。全文没有花哨的理论都是可以直接拿去用的SQL和排查思路。1. 事件调度器在系统里的位置为什么我最后选它1.1 某些场景是 Cron 解决不了或者解决得很别扭的用系统Cron调度数据库任务本质上是在“外部”驱动“内部”。你需要持续维护三样东西Cron任务本身、连接数据库的客户端、存放连接信息的配置文件。比如团队里四个人每个人机器上可能都挂着一份清理脚本某次数据库密码轮换之后漏改一台机器就能让某个定时任务凭空消失。Cron的日志又很克制默认情况下任务跑到一半挂了不会有人知道你只能在第二天发现数据异常后回查。事件调度器不一样。它运行在MySQL服务进程内部周期性任务不再需要独立的客户端和脚本文件只要数据库本身活着事件就有机会被执行。更重要的是事件也是数据库对象可以纳入权限管理、记录到元数据表、写入binlog和表、视图、存储过程享受同一套管理规范。对纯数据库内的周期任务来说把Cron这层外部依赖剥掉运维心智负担会小很多。不过也要说清楚事件调度器并不是要把系统Cron完全替代。它更适合处理“计算内容几乎都在SQL里”的任务比如清理、归档、更新汇总、生成报表数据。如果任务需要调用外部接口、读写文件、跨异构系统编排那还是让专业调度器来干更合适。1.2 事件调度器的工作机制与选型判断事件调度器本质上是一个常驻后台的调度线程。你定义好事件之后MySQL会在设定的时间点或时间间隔触发对应的SQL语句或存储过程。它支持三种基本调度方式一次性任务用AT 时间点指定比如明天凌晨两点执行一次周期性任务用EVERY 间隔指定比如每30分钟一次时间窗口在周期任务上叠加STARTS和ENDS限制任务只在某个时间段内生效。事件体可以是一条SQL也可以是一个多语句的BEGIN...END块还可以直接CALL某个存储过程。正是这个灵活性让它能承接不少相对复杂的批处理逻辑。选型时我一般用一张表快速判断对比维度系统Cron mysql客户端MySQL事件调度器执行入口依赖操作系统调度需要额外安装客户端MySQL内置调度线程无外部依赖连接配置连接串、密码容易散落在多台机器免连接直接在库内执行权限管控靠脚本权限粒度粗依赖MySQL的EVENT权限可控失败反馈默认无通知需自行写日志和告警可以写库内日志表配合监控时区问题跟随操作系统时区跟随MySQL时区和time_zone设置跨实例能力可以在脚本里连多个实例单个MySQL实例内运行一句话结论任务处理的数据都在同一套MySQL实例里就优先用事件任务要跨库、跨中间件、跨机器编排就别硬塞给事件。2. 从零搭一个事件开关、权限、语法三件事2.1 先确认调度器开着且重启后依旧开着MySQL的事件调度器默认是关闭状态。也就是说你创建了事件但它不会跑很多人第一次试就在这里卡住。检查当前是否开启SHOW VARIABLES LIKE event_scheduler;看到的结果只有三种ON、OFF、DISABLED。DISABLED是启动时被硬性指定的状态这种状态下运行期无法直接开启必须修改配置文件后重启服务。所以最稳妥的做法是直接在MySQL配置文件my.cnf的[mysqld]段里写一行[mysqld] event_schedulerON如果是临时调试可以用下面这句在线开启SET GLOBAL event_scheduler ON;注意SET GLOBAL只对当前运行周期有效服务重启后如果配置里没写会重新回到默认关闭状态。另外开启或关闭事件调度器需要SUPER或SYSTEM_VARIABLES_ADMIN权限普通账号不能随便操作。确认开启后你可以在SHOW PROCESSLIST里看到一个名为event_scheduler的常驻线程它就在后台等着按计划触发事件。创建事件本身需要数据库级别的EVENT权限一般建议单独建一个任务账号来管理不要用业务账号乱创建。授权语句类似GRANT EVENT ON your_db.* TO task_userlocalhost;2.2 CREATE EVENT 核心语法一个每分钟心跳的例子先看一个最简单的、每个人都能马上跑通的例子往日志表每分钟写一条心跳记录。CREATE EVENT heartbeat_event ON SCHEDULE EVERY 1 MINUTE DO INSERT INTO operation_log(message, create_time) VALUES (heartbeat, NOW());这条语句逐段拆开看CREATE EVENT heartbeat_event事件名在同一个实例里建议带业务前缀方便管理ON SCHEDULE EVERY 1 MINUTE周期调度每1分钟触发一次DO到点后要执行的内容这里是单条插入语句。创建完之后等一分钟再查这张表如果不断有新数据写入说明事件已经在跑了。你还可以通过information_schema.events查看它的执行轨迹SELECT event_name, status, last_executed FROM information_schema.events WHERE event_name heartbeat_event;LAST_EXECUTED字段会显示上一次实际执行的时间这是判断事件是否真正跑过的最直接证据。如果事件体里有多条语句就没法在DO后面用单条SQL搞定了。需要写成BEGIN...END块并且在命令行客户端里临时修改分隔符DELIMITER $$ CREATE EVENT two_step_event ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 02:00:00 DO BEGIN UPDATE summary_table SET status done WHERE day_id CURDATE() - INTERVAL 1 DAY; INSERT INTO task_log(task_name, run_time) VALUES (daily_summary, NOW()); END$$ DELIMITER ;注意这里我用了STARTS指定从某个时间点开始生效。如果不写STARTS周期事件默认立即开始计时。一次性事件则用ATCREATE EVENT one_time_event ON SCHEDULE AT 2025-06-01 10:00:00 DO UPDATE notice SET is_published 1 WHERE publish_time 2025-06-01;默认情况下一次性事件执行完就会被系统自动删除也就是ON COMPLETION NOT PRESERVE。如果希望它执行完保留记录可以加上ON COMPLETION PRESERVE这样事件还在只是不会再触发。3. 事件日常管理看状态、暂停修改、安全下线3.1 从 information_schema.events 里读出事件全貌事件创建多了之后管理页面不可能靠脑子记。我习惯用这句话查全实例的事件SELECT event_schema, event_name, definer, status, event_type, execute_at, interval_value, interval_field, starts, ends, last_executed FROM information_schema.events ORDER BY event_schema, event_name;这个查询的价值在于它把每个事件的调度状态、启停时间、上次执行时间全部摊开了。想发现问题直接看STATUS是否为ENABLED看LAST_EXECUTED是否长时间没更新基本能判断个大概。SHOW EVENTS这个命令也能看但它默认只显示当前数据库下的事件跨库管理时代不方便不如直接查information_schema.events来得全。还有一点要特别提醒不要去直接修改系统库里的mysql.event表那张表在新版本里属于数据字典的一部分手工改它很容易造成元数据损坏。事件的可视化管理就通过CREATE EVENT、ALTER EVENT、DROP EVENT这组语法完成。3.2 ALTER 和 DROP改周期、改内容、停用与删除发现某个事件跑得太频繁需要从每小时改成每天ALTER EVENT heartbeat_event ON SCHEDULE EVERY 1 DAY;想临时暂停任务但不删除事件定义ALTER EVENT heartbeat_event DISABLE;重新恢复ALTER EVENT heartbeat_event ENABLE;想彻底修改事件体里的SQLALTER EVENT heartbeat_event DO INSERT INTO operation_log(message, create_time) VALUES (heartbeat-2, NOW());想下线一个已经不需要的事件DROP EVENT IF EXISTS heartbeat_event;我的经验是生产环境里“停用某事件”和“删除某事件”是两件不同的事。遇到疑似有问题的事件先DISABLE保留现场确认任务被替代之后再做DROP。因为事件一旦删除只有当天的执行历史都一并消失了事后想复盘很难。4. 三个可以直接落地的场景清理、归档、汇总4.1 场景一过期数据自动清理附安全删除方案最典型的是日志表清理。约定保留90天每天凌晨2点30分执行CREATE EVENT clean_expired_log ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 02:30:00 DO DELETE FROM audit_log WHERE created_at NOW() - INTERVAL 90 DAY;这个写法功能上没问题但如果audit_log数据量大直接一次性DELETE会带来两个隐患一是单事务持有大量行锁执行期间可能阻塞业务写入二是主从复制时从库要应用一个大事务极易造成长时间延迟。我在生产环境里更倾向于把删除逻辑放进存储过程按批次循环删DELIMITER $$ CREATE PROCEDURE sp_clean_expired_log() BEGIN DECLARE v_rows INT DEFAULT 1; WHILE v_rows 0 DO DELETE FROM audit_log WHERE created_at NOW() - INTERVAL 90 DAY LIMIT 1000; SET v_rows ROW_COUNT(); DO SLEEP(1); END WHILE; END$$ DELIMITER ;然后事件里只需要一句话CREATE EVENT clean_expired_log ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 02:30:00 DO CALL sp_clean_expired_log();这样每次都只删一小批事务短锁持续时间短对主从复制的冲击也小。SLEEP(1)是我故意加进去的目的就是给数据库一个喘气空间。4.2 场景二按小时归档流水表流水表如果只保留最近7天数据就需要每小时把更早的数据搬进历史表。这类任务的关键是要保证“搬走”和“删掉”是一致的避免搬完没删造成重复或者删了没搬造成丢数据。简单版本如下CREATE EVENT archive_flow_log ON SCHEDULE EVERY 1 HOUR STARTS 2025-01-01 00:10:00 DO BEGIN START TRANSACTION; INSERT INTO flow_history SELECT * FROM flow_log WHERE created_at NOW() - INTERVAL 7 DAY; DELETE FROM flow_log WHERE created_at NOW() - INTERVAL 7 DAY; COMMIT; END;注意事件体里的多语句块不要忽略事务控制否则INSERT和DELETE各自自动提交一旦删除之后发现插入有问题很难回滚。真正的大表归档我不会用这种整批INSERT SELECT方式而是按小时或按天切片把“删除这7天前的数据”拆成多个时间段小任务避免一次性跨度过大。4.3 场景三统计汇总表定时刷新有些复杂的统计数据如果每次都实时COUNT、SUM查询很慢。更常规的做法是准备一张汇总表让事件定期刷新。比如每天凌晨统计前一天的订单汇总DELIMITER $$ CREATE EVENT refresh_daily_stats ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 01:00:00 DO BEGIN INSERT INTO daily_order_stats(stat_date, order_count, total_amount) SELECT DATE(created_at) AS stat_date, COUNT(*) AS order_count, COALESCE(SUM(amount), 0) AS total_amount FROM orders WHERE created_at CURDATE() - INTERVAL 1 DAY AND created_at CURDATE() ON DUPLICATE KEY UPDATE order_count VALUES(order_count), total_amount VALUES(total_amount); END$$ DELIMITER ;这里用ON DUPLICATE KEY UPDATE做增量刷新而不是先清空再全量重建好处是当天跑挂了第二天重跑一遍也能把数据补上。对实时性要求更低的报表也可以改成每小时全量TRUNCATE INSERT SELECT但要考虑窗口期里报表可能读到空数据。5. 事件不跑和重复跑的排查链路5.1 事件一直不执行从开关到时间窗口的逐项排查事件最让人头疼的问题就是“悄悄不跑”。我踩过一次相当无语的坑新定义的事件创建成功状态也显示ENABLED但过了很久LAST_EXECUTED一直是NULL。后来才发现问题是事件创建时用了一个STARTS时间结果当前系统时间早就越过ENDS结束时间事件等于在一个已经过去的时间窗口里“永久冷场”。为了避免这种问题我给自己整理了一条排查链路按顺序走确认调度器总开关SHOW VARIABLES LIKE event_scheduler;确认事件本身处于启用状态SELECT event_name, status, last_executed FROM information_schema.events WHERE event_name 你的事件名;确认时间窗口没有过期看STARTS、ENDS字段尤其是ENDS已经过去的事件即使状态是ENABLED也不会再触发。确认时区设置符合预期SELECT global.time_zone, session.time_zone;如果你的事件时间是凌晨2点但MySQL用的还是系统时区而系统时区恰好是UTC那实际执行时间就会偏移8小时。查看错误日志确认事件执行过程中是否抛了异常。事件里SQL报错不会立刻以告警形式通知你但错误日志里通常会有记录。确认事件权限没问题。事件体里操作的表如果不在事件所在库DEFINER账号必须对目标表有权限否则每次到点执行都会静默失败。如果实例是从库确认事件没有处于SLAVESIDE_DISABLED状态。这个状态表示事件被复制到从库后主动关闭了如果不小心在从库上ENABLE了也可能引发重复执行的问题。最后再看一眼SHOW PROCESSLIST确认有没有event_scheduler线程如果这个线程都不存在说明调度器根本没运行。5.2 主从环境里事件重复执行的两个典型路径主从环境是事件出问题的高发区。事件定义本身也是数据库对象在主库创建时CREATE EVENT会进入binlog从库应用后同样会存在这个事件。如果从库的事件调度器也开着两边会同时执行同一个任务清理任务重复跑还好最多浪费点资源如果是插入统计、发送通知这类任务就会造成重复数据。两条典型路径是事件在主库和从库都处于启用状态到点后主库执行一次从库又执行一次主库执行事件时产生的DML语句会写入binlog并复制到从库从库相当于把任务结果又“应用”了一遍叠加从库自身调度执行的结果等于同一个任务被放了至少两次。所以如果你维护的是主从或者MGR环境事件调度器必须明确角色分工调度任务只允许主角色执行。从库上有两种推荐做法。第一种直接关掉从库的调度器SET GLOBAL event_scheduler OFF;第二种在从库上把事件单独禁用ALTER EVENT clean_expired_log DISABLE ON SLAVE;这条命令执行后事件在从库上的状态会变成SLAVESIDE_DISABLED调度器不会再触发它而主库不受影响。如果架构是多个应用节点共享同一个库事件虽然只在一个库上但你仍然要小心多个节点各自都建同名事件结果到点以后同一实例里真的跑了两遍。这种场景我习惯在事件体里加一个互斥锁事件先尝试获取一个命名锁拿不到就直接退出。CREATE EVENT clean_expired_log ON SCHEDULE EVERY 1 DAY DO BEGIN IF GET_LOCK(clean_expired_log, 0) THEN CALL sp_clean_expired_log(); DO RELEASE_LOCK(clean_expired_log); END IF; END;用这种方法即使事件被误操作成多个实例同时触发同一时刻也只有一个实例能真正拿到锁执行任务。6. 事件运维兜底监控痕迹、权限收口、能力边界6.1 让事件留下执行记录日志表与异常捕获事件执行的时间点再准没有执行记录就是黑盒。我给自己手上的关键事件统一加了一张执行日志表事件每次运行都往里写一条记录CREATE TABLE event_run_log ( id INT AUTO_INCREMENT PRIMARY KEY, event_name VARCHAR(64) NOT NULL, start_time DATETIME NULL, end_time DATETIME NULL, affected_rows BIGINT NULL, result VARCHAR(32) NULL, error_msg TEXT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后在存储过程里捕获异常无论成功还是失败都写日志。简化后的模板DELIMITER $$ CREATE PROCEDURE sp_task_with_log(IN p_event_name VARCHAR(64)) BEGIN DECLARE v_start DATETIME DEFAULT NOW(); DECLARE v_affected BIGINT DEFAULT 0; DECLARE v_error_msg TEXT DEFAULT NULL; DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN SET v_error_msg sql exception; END; -- 业务逻辑开始 DELETE FROM audit_log WHERE created_at NOW() - INTERVAL 90 DAY; SET v_affected ROW_COUNT(); -- 业务逻辑结束 INSERT INTO event_run_log(event_name, start_time, end_time, affected_rows, result, error_msg) VALUES (p_event_name, v_start, NOW(), v_affected, IF(v_error_msg IS NULL, SUCCESS, FAILED), v_error_msg); END$$ DELIMITER ;这样你只需要定期检查event_run_log哪次任务失败、耗时多久、影响了多少行全都有据可查。更进一步可以把“检查LAST_EXECUTED是否太旧”做成一个监控指标如果某个关键事件超过两个周期没有更新执行时间就说明任务已经哑了。6.2 不要硬上什么场景该换调度方案事件调度器在MySQL内部确实很顺手但它不是万能的编排平台。遇到下面这些情况就该换方案任务需要跨多个数据库实例或跨中间件执行比如从MySQL取数再写进Redis还要调用外部API任务需要完整的失败重试、超时控制、依赖等待、人工审批任务并发量很大需要成熟的分布式锁和调度队列任务需要毫秒级甚至更细的调度精度。事件调度器的最小粒度是秒级它能保证“到点就尝试触发”但不会为任务积压、重试、失败补偿提供完整能力。MySQL本身也不会在全实例关闭后继续追补错过的任务重启期间错过的调度就是错过了。所以我的定位一直是事件调度器适合承载“库里就能闭环”的定期SQL运维比如清理、归档、刷新汇总。一旦任务涉及库外系统、复杂编排和完整闭环还是老实把调度权交出去让专业的任务调度平台来做。这样事件只负责它最擅长的部分反而更稳定。如果你准备在你自己的项目里启用事件建议从最小的心跳事件开始跑通再逐步把清理、归档、统计刷新这类任务迁移进来同时把执行日志表和检查清单一并建立起来。这套东西维护成本很低但能帮你挡掉很多凌晨三点被叫起来处理数据的尴尬。
阅读完成 · 觉得有帮助?
咨询建站