ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL事件调度器实战:定时清理、汇总统计与避坑指南

MySQL事件调度器实战:定时清理、汇总统计与避坑指南 维护过几套业务后台的MySQL库之后你大概率会遇到这类需求每天凌晨把三个月之前的日志清掉、每小时自动把订单表里的超时单标记为失效、每周一自动生成上周的统计报表。很多人第一反应是引入一套定时任务框架或者直接在应用里写调度代码。但如果你做的只是“跟数据库强相关”的周期性操作MySQL自带的事件调度器Event Scheduler可能才是最短路径。这也是我最早接触“MySQL数据库事件”这个功能时最直观的感受它就是一个跑在数据库内部的定时任务不需要额外部署Worker也不需要改应用代码把SQL写在事件里到点自动执行。这篇文章想系统梳理一下我对MySQL事件调度器的完整理解从为什么用它、怎么开启、语法怎么写到实际能落地的几个场景以及我这些年踩过的坑。无论你是刚接触MySQL的初级开发还是在维护生产库的DBA照着这篇文章能把“定时任务”这件事在MySQL里跑通并且能避开大部分隐藏问题。1. 数据库事件到底解决了什么问题先搞清楚使用边界1.1 事件调度器是什么官方文档里管它叫Event Scheduler翻译成“事件调度器”也好“数据库事件”也好本质就是一个在MySQL实例内部运行的定时任务引擎。它允许你定义一个命名对象让数据库按照预设的时间规则自动执行一条或多条SQL语句。它和存储过程、触发器经常被放在一起讨论但三个东西的定位完全不同。存储过程是“被动调用”的你需要显式CALL它才会执行触发器是“事件驱动”的表上发生INSERT、UPDATE、DELETE时自动触发而事件是“时间驱动”的到了设定好的时间点MySQL自己就会执行不需要外部干预。从MySQL 5.1.6开始这个功能就内置了。它支持两种调度方式一种是一次性任务比如“2024年12月31日晚上10点执行一次”另一种是周期性任务比如“每隔6小时执行一次”“每天凌晨2点执行”。实际业务里后者使用频率远高于前者绝大多数场景都是周期性任务。1.2 事件方案与其他定时任务方案的选型对比我见过很多团队一上来就上xxl-job、Spring的Scheduled或者直接在操作系统里配crontab不太清楚MySQL本身就能干这件事。这里不是说哪个方案最好而是要根据任务本身的依赖来选。方案执行位置适用场景典型问题MySQL事件调度器数据库内部数据清理、数据归档、汇总统计、状态流转SQL能搞定的事无法调用外部接口分布式环境下要处理多库重复问题Linux crontab操作系统需要执行Shell脚本、调用外部程序或操作多个数据库跨平台差依赖服务器环境脚本挂了没有自愈机制Spring Scheduled应用进程内业务逻辑刚需如发短信、推送、调用第三方接口多实例部署时容易重复执行需要配合分布式锁xxl-job等分布式调度独立调度中心需要失败重试、任务分片、可视化运维的团队级场景引入额外的部署和运维成本我的选择原则很简单如果这个定时任务的逻辑完全是SQL能完成的优先考虑MySQL事件。比如清日志、删过期数据、更新状态位、生成汇总快照这些操作天然跟数据库绑定直接在库里跑最直接还省掉了应用层和数据库层的网络开销。反过来如果任务需要调用HTTP接口、发送邮件、操作文件系统那就应该放到应用层的调度框架里去不要硬塞给事件调度器。2. 事件调度的第一步把开关打开并确认状态2.1 查看与开启event_scheduler创建一个事件之前先确认MySQL实例上的事件调度器是否已经开启。这个功能默认在MySQL 8.0里是开启的但老版本或者一些云数据库实例上可能是关闭的。你直接执行SHOW VARIABLES LIKE event_scheduler;返回结果有两种可能------------------------ | Variable_name | Value | ------------------------ | event_scheduler | ON | ------------------------或者------------------------ | Variable_name | Value | ------------------------ | event_scheduler | OFF | ------------------------如果当前值是OFF执行下面的语句临时开启SET GLOBAL event_scheduler ON;这个操作不需要重启数据库立即生效。但要注意它只管当前实例运行期间MySQL重启之后又会回到配置文件里设定的状态。2.2 让配置在重启之后继续生效如果你的库重启后事件还是不执行八成是配置文件里没写死。Linux环境下编辑my.cnfWindows环境下编辑my.ini在[mysqld]段下面加一行[mysqld] event_schedulerON改完重启MySQL服务之后这个开关就会一直保持开启。还有一个小细节通过SET GLOBAL方式开启事件调度器时当前会话用的是哪一个账号我建议用有SUPER权限的账号执行否则可能没有权限修改全局变量。另外MySQL 8.0里可以通过SELECT event_scheduler;来查看变量值和SHOW VARIABLES效果一样看你习惯哪种写法。3. CREATE EVENT完整语法拆开每一个选项讲清楚3.1 调度时间是怎么写的创建事件的核心语法如下CREATE EVENT [IF NOT EXISTS] event_name ON SCHEDULE schedule [ON COMPLETION [PRESERVE | NOT PRESERVE]] [ENABLE | DISABLE | DISABLE ON SLAVE] [COMMENT comment] DO do_program;其中schedule是精髓它有两种形式AT timestamp [ INTERVAL interval] -- 一次性任务到指定时间点执行一次 EVERY interval [STARTS timestamp] [ENDS timestamp] -- 周期性任务每隔一个时间单位执行一次interval的写法是“数量 单位”单位可以是YEAR、QUARTER、MONTH、WEEK、DAY、HOUR、MINUTE、SECOND比如EVERY 1 DAY EVERY 6 HOUR EVERY 30 MINUTE EVERY 1 WEEK这里有个非常容易写错的地方EVERY 1 DAY表示每24小时执行一次但如果某个任务要求“每个自然日执行一次”——比如每天凌晨2点跑一次日结——正确的写法不是EVERY 1 DAY而是EVERY 1 DAY STARTS 2024-01-01 02:00:00加了STARTS之后MySQL会从指定的时间点开始起算周期。比如STARTS是2024-01-01 02:00:00那么第一次执行就是2024-01-01 02:00:00第二次是2024-01-02 02:00:00。如果不加STARTS则从事件创建时刻开始每隔24小时执行一次创建时间如果不是凌晨2点执行时间点就会一直偏移。这个坑我在生产环境里踩过一次后来每建事件都会下意识检查STARTS。3.2 ON COMPLETION、ENABLE/DISABLE、DEFINER这些次要参数的坑有几个参数平时不起眼但出了问题很要命。ON COMPLETION PRESERVE表示事件执行完之后保留定义ON COMPLETION NOT PRESERVE表示执行完后自动删除。对于一次性事件AT方式默认是NOT PRESERVE执行完就没了。如果你希望一次性事件执行完了还能保留定义以便审计记得加上ON COMPLETION PRESERVE。对于周期性事件这个参数影响不到什么因为循环事件不会自动完成除非设置了ENDS时间。ENABLE和DISABLE控制事件是否生效。建好事件后如果你想临时停掉它但不想删定义用ALTER EVENT event_name DISABLE;即可后面再ENABLE恢复。真正容易出问题的是DEFINER即事件的定义者。事件创建后执行时会以定义者的权限去执行其中的SQL。如果定义者账号权限被收回或者主从复制中定义者不存在事件会执行失败。我见过一个案例运维用root账号建了一堆事件后来做账号安全治理把root禁用了第二天所有事件集体失效。排查半天最后发现是DEFINER指向了一个失效账号。所以规范的做法是给事件单独建一个专用账号比如event_runner只授予必要的库和表权限然后用它来创建事件。4. 三个拿来就能用的实战案例4.1 日志表定期清理为什么把时间点定在凌晨3点日志清理是最常见的数据库事件应用场景。业务日志、操作日志只增不减时间长了磁盘撑不住查询也变慢。我习惯的做法是保留90天数据每天清理一次。先建一张日志表模拟场景CREATE TABLE operation_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(500), created_at DATETIME NOT NULL, KEY idx_created_at (created_at) );然后创建事件CREATE EVENT ev_cleanup_operation_log ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 03:00:00 ON COMPLETION PRESERVE DO BEGIN DELETE FROM operation_log WHERE created_at NOW() - INTERVAL 90 DAY; END;这里有几个可以展开的细节。第一为什么选择凌晨3点而不是凌晨0点很多系统在0点前后有日切、批处理、数据扎帐之类的任务此时数据库负载往往偏高。把清理任务错峰到凌晨3点能避免和核心业务任务抢资源。当然如果是纯内部系统0点也可以但这个“错峰”的思维是通用的。第二DELETE大表数据要分批。如果数据量到达千万级别上面的写法一次删太多会产生大事务带来主从延迟和锁竞争。更稳的写法是每次只删1万行循环删除CREATE EVENT ev_cleanup_operation_log_batch ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 03:00:00 ON COMPLETION PRESERVE DO BEGIN DECLARE delete_count INT DEFAULT 1; WHILE delete_count 0 DO DELETE FROM operation_log WHERE created_at NOW() - INTERVAL 90 DAY LIMIT 10000; SET delete_count ROW_COUNT(); END WHILE; END;这个版本里用了ROW_COUNT()来获取当前DELETE影响的行数删到影响行数为0时退出循环。需要注意的是如果一次循环里删了大量行仍然可能产生大事务更彻底的做法是内部再加一个DO SLEEP(1);让两次删除之间喘口气减少对binlog和InnoDB刷盘的压力。4.2 每日销售汇总左闭右开区间统计的写法定时汇总统计也特别适合用事件来做。比如每天把前一天的订单统计结果写入一张汇总表第二天早上报表直接查汇总表不用再去扫订单流水。建一张订单表和一张汇总表CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32), amount DECIMAL(10,2), order_date DATETIME, status TINYINT ); CREATE TABLE daily_sales_summary ( stat_date DATE PRIMARY KEY, total_amount DECIMAL(12,2), order_count INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );创建事件CREATE EVENT ev_daily_sales_summary ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 02:10:00 ON COMPLETION PRESERVE DO BEGIN INSERT INTO daily_sales_summary (stat_date, total_amount, order_count) SELECT CURDATE() - INTERVAL 1 DAY, IFNULL(SUM(amount), 0), COUNT(*) FROM orders WHERE order_date CURDATE() - INTERVAL 1 DAY AND order_date CURDATE(); END;这个案例里的关键点是区间写法。统计“前一天”的数据很多人会写WHERE DATE(order_date) CURDATE() - INTERVAL 1 DAY这样也能查出结果但对order_date字段套了DATE()函数后索引就失效了订单表大了之后查询会扫全表。正确写法是 前一天零点 AND 今天零点这是一种左闭右开区间能充分利用order_date上的索引。4.3 从事件中调用存储过程复杂业务逻辑的拆分事件里的DO子句可以直接写多条SQL但一旦逻辑复杂——比如有多个步骤中间还要做判断——写在事件里会非常臃肿也不方便调试。这种情况我会先把逻辑封装成存储过程事件里只保留一句CALL。举个例子电商系统里超过30分钟未支付的订单要自动关闭。先写存储过程DELIMITER $$ CREATE PROCEDURE pro_close_timeout_orders() BEGIN UPDATE orders SET status 5, close_reason timeout, close_time NOW() WHERE status 1 AND order_date NOW() - INTERVAL 30 MINUTE; END$$ DELIMITER ;然后创建事件CREATE EVENT ev_close_timeout_orders ON SCHEDULE EVERY 5 MINUTE ON COMPLETION PRESERVE DO BEGIN CALL pro_close_timeout_orders(); END;这里为什么要用“每5分钟扫一次”而不是“精确到秒的定时器”因为数据库事件的最小调度单位是秒但业务上没必要让数据库每秒钟都去扫一批可能需要被关闭的订单5分钟跑一次UPDATE只更新状态为“待支付”的记录压力完全可以接受。而且这样设置之后事件本身的执行时间也短不容易产生锁等待。把存储过程和事件分开还有一个好处将来如果业务逻辑变了只需要ALTER PROCEDURE不需要动事件的调度配置。如果临时想马上关掉一批超时单也可以直接CALL一次存储过程不用等事件下次触发。这种“调度与逻辑分离”的思路在维护成本上比把所有SQL堆在事件里要舒服很多。5. 事件日常管理查看、修改、删除与执行情况追踪5.1 常用的管理命令事件建好之后日常运维离不开几类查询和操作。推荐把下面这组命令收进自己的运维手册。查看当前库里的所有事件SHOW EVENTS;查看事件的建表定义SHOW CREATE EVENT ev_cleanup_operation_log;从系统表里看更详细的信息比如创建时间、最后执行时间、状态SELECT EVENT_SCHEMA, EVENT_NAME, DEFINER, STATUS, EVENT_TYPE, EXECUTE_AT, INTERVAL_VALUE, INTERVAL_FIELD, STARTS, ENDS, LAST_EXECUTED, CREATED, LAST_ALTERED FROM information_schema.EVENTS WHERE EVENT_SCHEMA your_database;修改事件的定义或调度规则ALTER EVENT ev_cleanup_operation_log ON SCHEDULE EVERY 12 HOUR;临时停用事件ALTER EVENT ev_cleanup_operation_log DISABLE;恢复启用ALTER EVENT ev_cleanup_operation_log ENABLE;删除事件DROP EVENT IF EXISTS ev_cleanup_operation_log;这些命令里我实际用得最多的是查information_schema.EVENTS因为它的LAST_EXECUTED字段直接告诉我一个事件上一次有没有正常运行。排查“事件是否执行”的问题时先看这个字段比看什么日志都有效。5.2 如何确认事件真的执行成功了MySQL事件执行成功后默认不会在业务表里打日志。也就是说除了information_schema.EVENTS里的LAST_EXECUTED会更新你没有任何直接手段知道“它执行了几行、有没有报错”。生产环境里我会建议给关键事件配一张执行日志表。先建表CREATE TABLE event_execution_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, event_name VARCHAR(100), executed_at DATETIME, affected_rows BIGINT, status VARCHAR(20), message VARCHAR(500) );然后在事件里顺手把执行情况写进去。以清理日志的事件为例CREATE EVENT ev_cleanup_operation_log ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 03:00:00 ON COMPLETION PRESERVE DO BEGIN DECLARE rows_affected BIGINT DEFAULT 0; DELETE FROM operation_log WHERE created_at NOW() - INTERVAL 90 DAY AND created_at 2025-01-01 00:00:00; SET rows_affected ROW_COUNT(); INSERT INTO event_execution_log(event_name, executed_at, affected_rows, status) VALUES(ev_cleanup_operation_log, NOW(), rows_affected, SUCCESS); END;如果事件里的SQL本身报错后面的INSERT就不会执行日志表里自然少了一条记录。每天早上看一眼日志表哪个事件没出现、哪个事件影响行数异常偏大心里马上就有数。6. 事件不执行、时间不准、主从重复常见问题排查清单6.1 开关和时区是最容易被忽视的两个隐形坑先说开关。很多人的事件不执行第一反应就是怀疑SQL写错了但其实先看一下SHOW VARIABLES LIKE event_scheduler;十有八九是OFF。MySQL重启之后这个值如果没有写到配置文件里会回到OFF状态所以记得按照上文2.2节的方式把它写进my.cnf或my.ini。再说时区。事件调度器完全依赖数据库的time_zone。如果MySQL配置的是系统时区而操作系统的时区被改过事件执行时间就会跟着跑偏。比如一个事件设定STARTS 2024-01-01 02:00:00如果数据库时区从东八区改成了其他时区实际触发时间可能就不是本地时间的凌晨2点。解决方法是把数据库时区固定下来。可以动态设置SET GLOBAL time_zone 08:00; SET time_zone 08:00;同时也建议写进配置文件[mysqld] default-time-zone 08:00这里有个容易忽略的点在MySQL客户端里单独SET time_zone只对当前会话有效。事件调度器是全局的所以必须用SET GLOBAL或者在配置文件里设置。另外配置时区时尽量用08:00这种固定偏移避免用操作系统时区名。6.2 权限、事务和主从库上的执行策略权限问题集中在DEFINER上。前面说过事件以定义者的权限执行。如果你用了event_runner账号创建事件之后这个账号的密码被改了或者账号被删了事件就会执行失败。排查方法是查看information_schema.EVENTS里的DEFINER字段确认它指向的账号是否仍然有效。事务和锁的问题更隐蔽。一个事件如果执行大事务比如一次性DELETE上百万行不仅自身耗时长还会导致主从延迟甚至锁住其他业务的读写。我之前遇到过凌晨清理事件和某个报表查询互相锁死的情况原因就是DELETE范围太大锁定了大量行。解决的思路就是像4.1节那样限制影响行数并且把任务拆成小批次执行。主从环境是另一个高频踩坑点。如果你有主从复制事件默认只应该在主库开启从库的event_scheduler保持OFF。否则主库执行一次从库也执行一次数据就会被重复处理。MySQL还专门提供了DISABLE ON SLAVE这个参数创建事件时加上它事件在从库上自动保持禁用CREATE EVENT ev_cleanup_operation_log ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 03:00:00 DISABLE ON SLAVE DO BEGIN DELETE FROM operation_log WHERE created_at NOW() - INTERVAL 90 DAY; END;需要注意事件DDL本身会通过binlog同步到从库从库的事件定义是存在的只是被标记为禁用。如果你从库的event_scheduler保持OFF再加上DISABLE ON SLAVE双保险基本就不会因为主从重复执行而出事故了。7. 写在使用MySQL事件几年之后几点个人体会数据库事件这个功能乍一看不如应用层定时任务框架“高级”但它最大的价值就是轻和近。轻是因为它不需要额外部署任何东西不引入新的运维负担近是因为它直接在数据所在的地方执行对数据量大的批量操作尤其友好。我自己使用它时有一条原则事件里只做纯数据的活不做需要外部依赖的事。比如清理、聚合、状态更新这些放在事件里非常顺手。但凡是需要调接口、发消息、读写外部存储的事一律排除在事件之外交给应用层调度去处理。守住这个边界事件的可靠性会高很多也不会被写成一个大杂烩。最后分享一个很小的习惯。凡是线上数据库里新建的事件我都会加COMMENT说明它的用途和负责人类似这样CREATE EVENT ev_cleanup_operation_log ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 03:00:00 ON COMPLETION PRESERVE COMMENT 清理三个月前的操作日志负责人张三 DO ...这个习惯最开始是出于团队协作的考虑后来我发现真正受益的是几个月后的自己。很多数据库的定时任务跑着跑着没人关心它是干什么的加一行COMMENT后面接手的人能少走很多弯路。定时任务这种事越清晰越不容易出错。
返回列表