尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL事件调度器实战:从crontab到数据库内置定时任务

MySQL事件调度器实战:从crontab到数据库内置定时任务 开了个新项目数据库里有个表每天都在涨白天业务高峰期不敢动只能等凌晨没人用的时候做点清理。一开始我图省事在Linux服务器上挂了条crontab半夜调mysql -e去执行SQL。后来数据量上来了光清理逻辑就写了一长串cron脚本越堆越乱换台机器部署还得重新配一遍。后来我把这套定时任务从“外部脚本”改成了MySQL自带的事件调度器用数据库事件直接在库里定义调度规则到点自己执行干净利落。这篇文章就是我对MySQL事件调度器的一次系统总结从最基础的原理、语法拆解到生产环境的实战案例和排查思路都过了一遍。注意这里的“事件”不是前端里那些click、mouseover的DOM事件而是MySQL内置的Event Scheduler也就是数据库自己的定时任务。如果你在网上搜“事件”搜出一堆JS相关的内容不用怀疑方向不一样。玩过Linux crontab或Java定时任务比如Spring Boot的Scheduled、XXL-Job的话理解起来会很快完全没接触过也没关系这篇从开关、权限、语法一步步来跟着操作就能跑起来。1. 事件调度器到底能干什么先认清它的边界1.1 它和系统crontab、应用层定时任务有什么区别MySQL事件调度器本质上是一个在数据库实例内部运行的调度引擎你只需要用一条CREATE EVENT语句把“什么时候执行、执行什么SQL”定义好剩下的交给数据库自己。它不像crontab那样依赖操作系统也不像应用层定时任务那样要养着一个服务进程。这三种定时方案我分别用过一段时间各自的优劣还是挺明显的对比维度系统crontab应用层定时任务Spring Boot、XXL-Job等MySQL事件调度器调度位置操作系统层面应用服务内部数据库实例内部外部依赖需要部署机器、配置环境需要应用服务常驻、任务管理平台无外部依赖数据库启动即可适合场景调外部脚本、做系统级备份、ETL同步复杂业务编排、分布式调度、失败重试告警库内数据维护、SQL级定时操作可移植性换机器要重新配应用部署时统一管理跟着库走SQL导出到新环境即可扩展能力可以写任意shell脚本很强可发消息、可调API较弱基本只能执行SQL和存储过程我遇到过很多同学一上来就想用一个方案包打天下。比如想用MySQL事件去发HTTP请求、发钉钉告警这其实就超出了事件的职责范围。反过来如果只是每天把日流水汇总进报表表却专门搭一套XXL-Job再写个Java服务那也有点杀鸡用牛刀。我的判断标准很朴素操作对象是库里的数据、逻辑能用SQL表达优先用事件调度器要碰外部系统才考虑crontab或应用层任务。1.2 事件调度器的适用场景和“不要碰”的场景我现在在项目里用事件调度器主要集中在下面这五类场景定时清理和归档比如日志表、操作流水表、临时数据表超过一定时间的数据自动删除或转移到历史表。定时汇总统计每天深夜把前一天订单、流量等明细数据聚合成汇总行写入报表表白天查询直接读汇总结果。定时刷新中间表把多表关联、复杂计算提前算好放到一张宽表里提供给BI或前端查询能省掉大量实时计算开销。定时维护数据库对象比如定期ANALYZE TABLE更新统计信息防止优化器走错执行计划。定时配合存储过程把一段复杂的循环、分批删除、异常捕获逻辑写进存储过程事件只需要CALL一下。如果你正在用KettleSpoon这类ETL工具做数据同步通常还需要在外部配置调度来触发同步任务但如果只是“同步完之后把本地临时表里过期的数据清一清”这种收尾操作完全可以在数据库里加个事件让库内自己处理少一道外部依赖也省得在Kettle任务链里层层嵌套。那什么事情不能碰我的经验是这两条线第一凡是涉及外部交互的别用事件。事件内部很难可靠地调用外部接口、发送邮件、执行系统命令。虽然可以通过自定义UDF扩展但UDF要么有安全风险要么维护成本高为了发个通知去装一个UDF不建议这么干。第二需要跨节点保证“只执行一次”的别用事件。事件在每个MySQL实例上都是独立运行的主从架构、多实例部署时如果不做额外控制每个实例都会执行一遍事件。这种情况更适合用带分布式锁的任务调度框架比如XXL-Job去统一调度。2. 动手前的准备开启调度器、权限与时区2.1 三步确认事件调度器状态很多人的事件“建好了就是不执行”第一个坑往往就是事件调度器根本没开。MySQL里控制调度器总开关的变量叫event_scheduler它有三种状态ON、OFF、DISABLED。默认情况下MySQL的event_scheduler是OFF也就是说你辛辛苦苦写好CREATE EVENT如果不先打开总开关事件等于一张废纸。先执行这条SQL确认当前状态SHOW VARIABLES LIKE event_scheduler;如果结果是OFF可以运行时直接开启SET GLOBAL event_scheduler ON;但这里有个细节ON和OFF之间可以运行时来回切换DISABLED不行。DISABLED是在实例启动阶段就被禁止调度器的状态它只能通过修改配置文件来改变。所以如果你执行了SET GLOBAL event_scheduler ON还是报错那就得检查一下my.cnfWindows上是my.ini里是不是写死了某些参数或者启动时用了--event-schedulerDISABLED。更稳妥的做法是直接把配置写进my.cnf的[mysqld]段[mysqld] event_scheduler ON改完重启MySQL后状态就是ON。我自己比较喜欢这种方式因为即使数据库实例意外重启调度器也会自动恢复开启不会出现“上次手开过重启后又失效”的隐患。开启之后可以用下面这条SQL看到后台已经多了一个event_scheduler线程SHOW PROCESSLIST;看到一条User为event_scheduler的线程就说明调度器已经在正常待命了。2.2 账号与权限为什么事件总是“权限不够”事件调度器不是魔法它执行SQL时同样需要权限校验。创建事件的账号必须具备EVENT权限否则会报“ERROR 1044 (42000): Access denied”。授权语句也不复杂GRANT EVENT ON yourdb.* TO app_userlocalhost; FLUSH PRIVILEGES;但真正容易踩坑的是事件“以谁的身份执行”这个问题。事件创建时会把DEFINER定义者一并记录下来之后事件执行SQL时用的就是DEFINER的权限而不是“谁启用了这个事件就用谁的权限”。这句话翻译成人话就是如果你的维护账号只有建事件权限没有对业务表的增删改查权限那事件到点执行时照样会报权限不足。我建议为定时任务单独准备一个运维账号把需要操作的表、存储过程的权限都授给它然后在创建事件时显式指定DEFINER。比如CREATE DEFINER opslocalhost EVENT ...账号规划这块不用搞得很复杂但一定要“明确这个事件跑起来用的是谁的身份”。否则排查问题时你会发现事件明明创建成功了也到时间了日志里却一堆PROCESS privilege denied之类的报错那种感觉相当抓狂。2.3 时区定时任务“看起来没到点”的元凶时区这个问题平时不显山不露水一出问题就非常迷惑主要症状是你定义“每天凌晨3点执行”结果它每天8点才跑或者干脆颠倒了12小时。原因其实很简单事件调度器在计算“什么时候该执行”时依赖的是MySQL的time_zone系统变量。先看当前时区设置SHOW VARIABLES LIKE time_zone;如果结果是SYSTEM说明MySQL跟随操作系统时间。操作系统时区一变MySQL的事件调度也跟着变。还有一种情况服务器为了跟国外系统对齐把系统时区设成了UTC而业务上要的是北京时间那“凌晨3点”实际就到了“上午11点”任务全挤在白天数据库直接被你干趴。我的做法是在配置文件里把时区固定下来不依赖操作系统[mysqld] default-time-zone 08:00这样MySQL的time_zone会显示为08:00跟业务时间保持一致不受系统时区跳变的影响。另外修改时区后要特别注意已经创建好的事件调度器会重新基于新时区计算执行时间不是你改一下它就自动“平移”的。如果改完时区发现一批事件的时间全乱了别慌逐个ALTER EVENT调整STARTS、ENDS即可。3. CREATE EVENT语法拆解从入门到能写生产级任务3.1 最小可用示例先建一张日志表验证学任何新东西我都喜欢先跑通一个最小示例再往深了学。MySQL事件的“Hello World”不复杂但有个前提得先说清楚事件的DO子句后面不能直接放一条SELECT语句然后期待把查询结果显示给你因为事件是在后台异步执行的没有客户端连接能把结果集返回给你。要验证事件有没有跑最直接的方式是让它写日志表。先建一张简单的日志表用来观察事件执行情况CREATE TABLE event_log ( id INT AUTO_INCREMENT PRIMARY KEY, event_time DATETIME, remark VARCHAR(100) );然后建一个每分钟执行一次的事件CREATE EVENT ev_minute_test ON SCHEDULE EVERY 1 MINUTE DO INSERT INTO event_log(event_time, remark) VALUES(NOW(), minute test);等一分钟后查这张表能看到数据不断插入就说明整个链路是通的。这个例子虽然简单但它把“事件执行结果怎么被观察”这个思路打通了后面所有复杂事件我都建议保留一条“写日志”的路径。3.2 ON SCHEDULE调度表达式详解AT、EVERY、STARTS、ENDSON SCHEDULE是事件定义里最核心的部分它决定了任务什么时候执行、按什么频率重复。MySQL不像cron那样有“每月第一个周一”这种复杂的字段组合它的调度表达式主要由AT和EVERY两种构成。一次性任务用AT比如指定一个具体时间点执行CREATE EVENT ev_once_cleanup ON SCHEDULE AT 2025-01-01 03:00:00 DO DELETE FROM temp_table WHERE expired 1;也可以用相对时间比如“从现在起1小时后执行”CREATE EVENT ev_once_hour_later ON SCHEDULE AT (CURRENT_TIMESTAMP INTERVAL 1 HOUR) DO INSERT INTO event_log(event_time, remark) VALUES(NOW(), one hour later);循环任务用EVERY后面跟一个时间单位和可选起点终点语法是EVERY interval [STARTS timestamp] [ENDS timestamp]interval的单位支持YEAR、QUARTER、MONTH、WEEK、DAY、HOUR、MINUTE、SECOND。举三个我实际用过的例子每天凌晨3点清理日志CREATE EVENT ev_clean_logs ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 03:00:00 DO DELETE FROM operation_log WHERE create_time NOW() - INTERVAL 90 DAY;每周日凌晨3点半执行统计数据汇总CREATE EVENT ev_weekly_summary ON SCHEDULE EVERY 1 WEEK STARTS 2025-01-05 03:30:00 DO CALL sp_generate_weekly_summary();从现在起每6小时执行一次直到某个时间点结束CREATE EVENT ev_six_hour_refresh ON SCHEDULE EVERY 6 HOUR STARTS (CURRENT_TIMESTAMP INTERVAL 1 HOUR) ENDS 2025-06-30 23:59:59 DO CALL sp_refresh_materialized();这里要特别提醒一下STARTS这个坑如果你的STARTS写的是一个过去的时间点MySQL不会帮你从过去补执行但它会认为“这个事件已经到点该跑了”于是创建完事件后可能立刻先执行一次。如果你不希望创建完马上跑一遍STARTS一定要写成未来的时间或者用CURRENT_TIMESTAMP INTERVAL做相对起点。3.3 DO子句的进阶写法BEGIN...END和存储过程DO子句后面可以写一条SQL也可以用BEGIN...END包一段复合语句。但有一个问题MySQL客户端的默认分隔符是分号;如果你在DO后面写多条SQL需要用DELIMITER命令临时把分隔符换成别的否则MySQL会在第一个分号处就认为语句结束了。下面是一个包含复合语句的事件示例DELIMITER $$ CREATE EVENT ev_multi_statement ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 02:00:00 DO BEGIN INSERT INTO event_log(event_time, remark) VALUES(NOW(), start); UPDATE summary_table SET total total * 1.01; DELETE FROM temp_data WHERE status DONE; END$$ DELIMITER ;实际上我自己写生产级事件时DO里通常只写一句CALL 存储过程()。把复杂逻辑全放进存储过程有几个好处存储过程可以在命令行里单独CALL调试报错时定位范围小事件里就算要多步操作存储过程内部可以写事务控制、异常捕获换环境部署时存储过程随数据库一起迁移事件定义也只保留一行CALL清晰得很。3.4 容易被忽略的状态选项ON COMPLETION、ENABLE/DISABLE、COMMENT很多新手写CREATE EVENT时把ON SCHEDULE和DO写完就结束了另外几个选项看都不看。但恰恰是这些小选项在生产环境里非常关键。ON COMPLETION控制一次性事件执行完后怎么处理ON COMPLETION PRESERVE默认值是ON COMPLETION NOT PRESERVE意思是一次性任务执行完成后MySQL会自动把这个事件删除。用完即焚在某些场景下很合适但如果你想保留事件定义用来检查历史执行情况就得写PRESERVE。英文单词容易给人错觉很多人以为PRESERVE是“保持启用状态”其实它保存的是“事件定义本身”。这个理解千万别搞反。ENABLE / DISABLE / DISABLE ON SLAVE控制事件是否参与调度ENABLE默认就是ENABLE事件会按时执行。DISABLE则暂停执行但事件定义还在。DISABLE ON SLAVE是主从环境下专门给从库用的选项从库上创建事件时加上它可以避免事件在从库上重复执行这个后面会专门说。COMMENT就是给事件加一行注释团队协同时强烈建议写。比如CREATE EVENT ev_clean_logs ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 03:00:00 COMMENT 每天清理90天前的操作日志负责人张三 DO DELETE FROM operation_log WHERE create_time NOW() - INTERVAL 90 DAY;半年之后你自己看到这行注释都能一眼想起这个任务是干嘛的比翻代码、查文档快得多。4. 事件的管理查看、暂停、修改、删除4.1 查看事件SHOW EVENTS和information_schema事件创建完之后最常用的操作就是查看它还活着没有、上次执行是几点。MySQL提供了两条路径SHOW EVENTS和information_schema.EVENTS表。SHOW EVENTS语法很简洁但它只是清单视图信息不算完整SHOW EVENTS FROM yourdb\G从输出里你能看到每个事件的Event_name、TypeRECURRING是周期事件ONE TIME是一次性事件、StatusENABLED或DISABLED以及上一次执行时间Last_executed。如果想要更细的信息比如创建时间、执行时间范围、间隔大小直接查元数据表SELECT EVENT_NAME, STATUS, LAST_EXECUTED, STARTS, ENDS, INTERVAL_VALUE, INTERVAL_FIELD, EXECUTE_AT, DEFINER FROM information_schema.EVENTS WHERE EVENT_SCHEMA yourdb\G这里面LAST_EXECUTED特别值得盯紧。我在排查“事件有没有跑”时第一件事就是看这个字段。如果预期每10分钟执行一次的事件LAST_EXECUTED还停留在两小时前那基本可以断定调度链路出问题了再往下查就行。4.2 修改事件ALTER EVENT的完整用法事件建好后难免要调整比如发现清理频率太低、任务处理的数据越来越多需要换个策略。ALTER EVENT能改的东西基本覆盖了你创建时定义的所有内容。改调度周期注意要重写完整的ON SCHEDULE表达式ALTER EVENT ev_clean_logs ON SCHEDULE EVERY 2 DAY STARTS 2025-01-01 04:00:00;改成只执行一次ALTER EVENT ev_clean_logs ON SCHEDULE AT 2025-03-01 03:00:00;改事件执行的SQLALTER EVENT ev_clean_logs DO DELETE FROM operation_log WHERE create_time NOW() - INTERVAL 60 DAY;改事件名ALTER EVENT ev_clean_logs RENAME TO ev_clean_operation_logs;暂停和恢复事件ALTER EVENT ev_clean_logs DISABLE; ALTER EVENT ev_clean_logs ENABLE;这里有个小建议改完事件之后顺手查一下information_schema.EVENTS确认LAST_EXECUTED以外的字段都符合预期然后再等一个调度周期观察实际执行情况。不要改完就忘毕竟ALTER EVENT不会帮你提前验证新的调度表达式是否合理。4.3 维护窗口怎么处理批量禁用和恢复事件有的场景下你需要在一段时间内暂停所有事件。比如要做大版本升级、做数据迁移、批量改表结构这时候如果事件还在半夜执行很可能撞上正在进行的DLL操作产生锁等待甚至失败。最粗暴的做法是一个一个ALTER EVENT ... DISABLE但事件一多就烦了。我习惯写一个临时存储过程遍历库里的事件动态批量禁用DELIMITER $$ CREATE PROCEDURE sp_disable_all_events() BEGIN DECLARE done INT DEFAULT 0; DECLARE event_name VARCHAR(64); DECLARE cur CURSOR FOR SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA DATABASE() AND STATUS ENABLED; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done 1; OPEN cur; read_loop: LOOP FETCH cur INTO event_name; IF done 1 THEN LEAVE read_loop; END IF; SET sql CONCAT(ALTER EVENT , event_name, DISABLE); PREPARE stmt FROM sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END LOOP; CLOSE cur; END$$ DELIMITER ;恢复时写一个类似的存储过程把DISABLE换成ENABLE就行。在实际执行批量禁用之前最好先跑一遍SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA DATABASE()看清楚当前库里到底有哪些事件别把不该禁的也禁了。恢复的时候也要记得核对事件清单比如中途新建的事件可不能漏掉。5. 生产级实战四个可以直接抄的事件任务5.1 定时清理过期数据分批删除防止大事务业务表只留最近90天数据这是定时任务最常见的需求。很多人的第一版SQL是这样的DELETE FROM operation_log WHERE create_time NOW() - INTERVAL 90 DAY;数据量小的时候没问题一两万条删起来很快。可一旦日志表里有几千万条、甚至上亿条数据这一条DELETE就是一个超大事务后果很严重事务持锁时间过长业务写入被阻塞undo日志膨胀磁盘空间紧张主从复制可能延迟几十秒甚至几分钟。我在生产环境里的做法是把大删除拆成小批次。比如一个存储过程每次只删2000条删完就提交直到把所有过期数据清完DELIMITER $$ CREATE PROCEDURE sp_purge_old_logs(IN p_days INT, IN p_batch_size INT) BEGIN DECLARE v_cutoff DATETIME; DECLARE v_rows INT DEFAULT 1; SET v_cutoff NOW() - INTERVAL p_days DAY; WHILE v_rows 0 DO DELETE FROM operation_log WHERE create_time v_cutoff LIMIT p_batch_size; SET v_rows ROW_COUNT(); COMMIT; -- 每删完一批稍微歇口气降低对系统的影响 DO SLEEP(1); END WHILE; END$$ DELIMITER ;然后事件里每天凌晨调它CREATE EVENT ev_purge_old_logs ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 03:00:00 COMMENT 每天凌晨清理90天前的操作日志 DO CALL sp_purge_old_logs(90, 2000);注意如果表里数据量特别大第一次在执行窗口内未必能删完。这时候可以让存储过程一次删一批、然后睡1秒DO SLEEP(1)把资源占用摊开避免直接把数据库IO打满。如果90天数据量巨大我还会提前几天把清理窗口调宽比如任务从凌晨1点开始跑让它在低峰期慢慢磨完。还有一种情况是清理之外还要留档。那就别直接删先INSERT INTO operation_log_history SELECT ... WHERE create_time 截止时间然后只删已经成功归档的部分。归档和删除可以在同一个存储过程里串行执行用事务包起来保证不会出现“删了但没归档”或者“归档了没删干净”的中间状态。5.2 每日汇总统计先删后插保证幂等报表统计是另一个典型场景。每天凌晨把昨天所有订单按天、按渠道、按商品维度汇总到一张报表表白天运营看数据直接查汇总结果速度快、压力小。这类任务的痛点是“重复执行会产生重复数据”比如某天任务因为数据库抖动没跑你手动补跑一次结果报表表里同一个日期的数据插了两条汇总数字直接翻倍。解决思路很简单汇总任务做成幂等同一批数据不管跑几次最终结果都是一样的。具体做法是先删后插每次执行时先删除目标日期范围在报表表里的老数据再重新插入最新汇总结果。DELIMITER $$ CREATE PROCEDURE sp_daily_order_summary() BEGIN DECLARE v_report_date DATETIME; -- 这里以昨天为统计日期跑批时自然延迟一天 -- 如果当天早上要跑“昨天的数据”也可以灵活改为CURRENT_DATE - INTERVAL 1 DAY SET v_report_date DATE(NOW() - INTERVAL 1 DAY); -- 幂等先删掉昨天已有的汇总数据 DELETE FROM daily_order_summary WHERE stat_date v_report_date; -- 再插入最新汇总 INSERT INTO daily_order_summary(stat_date, channel_id, order_count, order_amount) SELECT DATE(o.create_time), o.channel_id, COUNT(*), SUM(o.amount) FROM orders o WHERE DATE(o.create_time) v_report_date GROUP BY DATE(o.create_time), o.channel_id; END$$ DELIMITER ;事件定义如下CREATE EVENT ev_daily_order_summary ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 02:30:00 COMMENT 每天凌晨汇总前一天的订单数据 DO CALL sp_daily_order_summary();用“先删后插”还有一个隐藏好处如果哪天任务执行到一半失败了你重跑一次老数据已经删掉了新数据还没插完整报表表里那个日期的数据是空的至少不会出现“新旧数据混在一起”的情况。再加上事件本来就支持手动CALL存储过程补数非常方便。5.3 定期刷新中间表RENAME TABLE原子切换中间表也有人叫宽表、预计算表在报表和查询优化里很常用。比如订单主表、商品表、门店表分散在多个库查询时动不动要关联五六张表线上慢查询一堆。解决方案是定时把关联结果算出来灌进一张宽表。问题是刷新宽表期间如果直接把老表DELETE再INSERT刚好有用户查询就会读到空数据或者半新半旧的数据。这里我常用一个小技巧临时表原子替换。先把数据全量刷到一张临时表刷完之后用两条RENAME TABLE把临时表和老表瞬间换过来。整个过程对外是无感的因为RENAME TABLE是原子操作查询要么读到旧表、要么读到新表不会出现读一半的状态。DELIMITER $$ CREATE PROCEDURE sp_refresh_order_wide_table() BEGIN -- 建一张临时宽表 DROP TABLE IF EXISTS order_wide_tmp; CREATE TABLE order_wide_tmp LIKE order_wide; -- 往临时表里灌数据 INSERT INTO order_wide_tmp (order_id, user_name, product_name, store_name, amount, create_time) SELECT o.order_id, u.user_name, p.product_name, s.store_name, o.amount, o.create_time FROM orders o JOIN users u ON o.user_id u.user_id JOIN products p ON o.product_id p.product_id JOIN stores s ON o.store_id s.store_id WHERE o.create_time DATE(NOW() - INTERVAL 1 DAY); -- 原子切换老表改名为备份表临时表改名为正式表 RENAME TABLE order_wide TO order_wide_old, order_wide_tmp TO order_wide; -- 清理备份表不保留旧数据 DROP TABLE IF EXISTS order_wide_old; END$$ DELIMITER ;事件里每小时调一次CREATE EVENT ev_refresh_order_wide ON SCHEDULE EVERY 1 HOUR COMMENT 每小时刷新订单宽表 DO CALL sp_refresh_order_wide_table();这里有个小地方容易踩RENAME TABLE的时候如果order_wide_tmp和order_wide_old刚好存在重名会执行失败。所以我在正式环境里会把临时表和备份表的名字设计成带时间戳或固定后缀的形式并在存储过程开头先DROP TABLE IF EXISTS order_wide_old清理上一次的残留。这个动作看起来不起眼但它能防止连续两次刷新之间发生命名冲突。5.4 定期维护统计信息ANALYZE TABLE需要克制MySQL优化器选择执行计划时依赖表的统计信息行数、区分度、索引基数等。当表数据量发生了大幅变化统计信息还停留在很久以前就可能出现索引明明存在优化器却偏不走索引的“灵异现象”。定期跑ANALYZE TABLE能强制更新统计信息让优化器基于最新数据做判断。对这个需求事件任务很直接CREATE EVENT ev_analyze_big_tables ON SCHEDULE EVERY 1 WEEK STARTS 2025-01-06 03:00:00 COMMENT 每周一凌晨对大表更新统计信息 DO BEGIN ANALYZE TABLE orders; ANALYZE TABLE operation_log; ANALYZE TABLE user_account; END;但这里必须克制。有的人图省事直接对整个库的所有表跑ANALYZE TABLE其实没必要。大表分析耗资源小表统计信息变化也不大跑一次纯属浪费。我会维护一张“重点表清单”只对数据量增长快的核心表做定期分析其他表要么不分析要么等大版本变更时手动跑一次。另外ANALYZE TABLE在MySQL 8.0 InnoDB引擎下会做在线操作对正常读写的影响比之前小但在几百GB的大表上执行时依然会有明显的IO消耗。所以事件调度时间我会选在低峰期避开业务忙碌时段。6. 踩坑记录与排查思路速查表6.1 事件建好了但不执行按这个顺序查事件不执行是新手最常遇到的问题。我在团队里带人的时候通常要求大家按固定顺序排查而不是毫无章法地乱试一通症状可能原因排查方法解决对策事件到点没跑全局调度器关闭SHOW VARIABLES LIKE event_scheduler;运行时SET GLOBAL event_scheduler ON;或改配置重启事件状态是DISABLED事件被手动禁用或创建时就禁用查information_schema.EVENTS的STATUS字段执行ALTER EVENT ev_name ENABLE;事件状态ENABLED但没执行调用的存储过程或SQL本身报错手工CALL存储过程或直接执行DO里的SQL修复SQL或存储过程逻辑Last_executed一直为空事件还没到设定的执行时间查STARTS、ENDS字段是否满足当前时间调整STARTS为合理未来时间事件执行时权限不足DEFINER账号缺少对应权限查看事件DEFINER核实账号权限授权或重建事件时指定有权限的账号这个表格基本覆盖了80%的“不执行”问题。实际排查时我每查完一项就打一个勾不要跳跃因为多问题叠加的情况也很常见比如“全局开关没开”和“存储过程报错”同时出现你只修一个任务照样不跑。6.2 时间到了没执行先查这三处有一类坑事件本身是ENABLED的调度器也是ON的但它执行时间跟预期对不上。这种问题十有八九出在时间相关的设置上。第一个检查点是时区。MySQL的time_zone是SYSTEM时会跟随操作系统时间。如果系统时区改成了UTC你的EVERY 1 DAY STARTS 2025-01-01 03:00:00实际就跑到了北京时间上午11点。这个我在生产环境里踩过还好当时是报表任务影响可控但排查过程让我记忆深刻。第二个检查点是STARTS的设置。如果你写的是STARTS (CURRENT_TIMESTAMP INTERVAL 1 HOUR)这类相对起点会在事件创建时计算一次并固定下来之后重启实例、修改时区这个“未来时间”不会自动跟着变。如果你希望事件每天固定时间执行最好直接用具体的日期时间比如STARTS 2025-01-01 03:00:00这样不受创建时间影响。第三个检查点是系统时间是否被跨步调整。比如有人手动date -s改了服务器时间或者NTP做了大幅校时可能导致事件调度器直接跳过了某个调度周期。数据库本身的LAST_EXECUTED字段会告诉你上次实际执行是几点如果它跳了几个周期基本就是时间被调整了。6.3 怎么定位事件内部SQL的具体报错事件本身的报错不会直接发给你它只会默默记录在MySQL的错误日志里。所以当事件“好像跑了但又没成功”时第一件事就是去翻错误日志。日志位置可以通过参数查SHOW VARIABLES LIKE log_error;打开对应文件搜索event_scheduler或Event Scheduler相关关键字往往能直接看到类似Failed to execute event ...的报错后面跟着具体的SQL错误码。如果错误日志里没有明显信息我会把事件DO里的SQL或存储过程拉出来手工执行一遍。比如事件调的是sp_daily_order_summary()就直接在命令行CALL sp_daily_order_summary();手工执行时如果报错问题一定在存储过程或底层表结构上如果手工执行成功那事件定义部分的ON SCHEDULE调度逻辑、权限、状态就要重新检查。还有一个进阶技巧给存储过程加一层异常捕获把报错信息写进自己的日志表DELIMITER $$ CREATE PROCEDURE sp_safe_daily_summary() BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN INSERT INTO event_error_log(event_name, error_time, error_detail) VALUES(sp_daily_order_summary, NOW(), check MySQL error log); END; CALL sp_daily_order_summary(); END$$ DELIMITER ;事件里调用sp_safe_daily_summary()而不是直接调原存储过程这样即使出错日志表里也会留下痕迹不会让你毫无头绪地瞎猜。6.4 生产环境容易踩的其他坑最后把我在生产环境里踩过或见过别人踩的坑集中说一下这些细节在文档里不容易翻到但遇到一次就很疼。主从环境下的重复执行。如果你有主从同步事件在主库执行时产生的DML会通过binlog同步到从库从库如果也定义了同样的ENABLE事件那数据就会重复处理一遍。正确做法是只在主库上启用事件从库里的事件定义要么DISABLE要么创建时带上DISABLE ON SLAVECREATE EVENT ev_clean_logs ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 03:00:00 DISABLE ON SLAVE DO DELETE FROM operation_log WHERE create_time NOW() - INTERVAL 90 DAY;一次性事件误用NOT PRESERVE。如果你创建的是AT型一次性事件又没写PRESERVE执行完事情就被自动删除了。事后想确认它上次什么时候跑的、跑了没根本查不到。我建议所有任务型事件都写成ON COMPLETION PRESERVE让事件定义留下来历史执行记录还能从元数据里看。事件里别放危险DDL。比如DROP TABLE、TRUNCATE TABLE这类语句一旦写进事件里到点自动执行误删数据就真的“自动化”了。如果确实要做表级别的清理我会在存储过程里加一道“目标表名白名单”判断防止表名被拼错或者参数传入异常值。事件数量别贪多。一个库塞几百个事件调度线程本身也会有调度成本。我见过有人把各种小逻辑全塞进事件里导致凌晨一堆任务排队互相影响。更好的做法是拆成有限的几个“调度入口”每个入口内部按优先级顺序执行多段操作或者把复杂调度交给外部任务平台XXL-Job等去控制。我个人在实际操作中的体会是MySQL事件调度器最适合扮演“数据库自带的运维机器人”这个角色凡是跟外部系统有交集的活就老老实实交给外部调度别硬塞给它。现在我对事件任务的管理已经形成了一套固定习惯每个事件必须带COMMENT写清楚用途和负责人创建事件的SQL脚本纳入版本库管理关键任务执行时往心跳日志表里写一条记录第二天早上扫一眼就能知道有没有漏跑。最后再分享一个小技巧新环境上线一批事件后先手工执行一次事件里的存储过程或SQL确认语句本身没有问题再打开ENABLE开关。这个动作看着简单却能在正式运行前把绝大部分坑都填平。
返回列表