聊到MySQL里的自动化运维,很多人第一反应是把定时任务写在业务代码里,或者直接丢给Linux的crontab去执行。实际上MySQL本身提供了一套"数据库内建的定时任务机制",也就是事件调度器(Event Scheduler),平时大家说的"MySQL事件""定时事件""CREATE EVENT",指的都是这套东西。它能让数据库自己按照时间规则去执行SQL语句,比如每天凌晨清理过期日志、每小时汇总一次统计数据、定期把冷数据归档到历史表,这些都能在数据库内部闭环完成,不用再依赖外部脚本。这篇文章我打算把MySQL事件从使用场景、运行机制、创建语法到常见坑完整梳理一遍,适合正在做数据运维、后端的同学参考,看完基本可以直接上手部署自己的定时任务。
1. 事件调度到底解决什么问题
1.1 先搞明白它和crontab的本质差异
MySQL事件调度器本质上是一个常驻在数据库内部的后台线程,它按照创建事件时定义的时间规则,周期性或一次性触发一段SQL语句。很多人第一次听到这个功能时,第一反应是"这跟crontab有什么区别,我在服务器上写个shell脚本调mysql -e不也一样吗"。
两者确实都能实现定时执行SQL,但差异非常大。crontab是操作系统层面的工具,它只管"到点执行某个命令",至于这个命令连不连得上MySQL、权限够不够、SQL有没有执行成功,它完全不关心。你需要额外处理连接管理、错误日志、密码安全问题。而事件调度器是数据库自己管理的一套机制,它直接在MySQL实例内部运行,不经过网络连接、不暴露密码、不需要额外部署脚本,执行失败也会记录到错误日志或binlog里,运维上更干净。
另外还有一个经常被忽略的点,crontab只能精确到分钟级别,而MySQL事件支持的最小粒度是秒。比如"每隔30秒清理一次在线状态表",这种短周期任务用crontab几乎做不到,用事件调度器却是很简单的事。如果你的业务需要数据库层面的秒级定时任务,事件调度器几乎是唯一合理的方案。
1.2 什么场景真正适合用事件调度器
根据我自己在多个项目里的实践,MySQL事件在下面几类场景中特别顺手,基本上属于"用了就回不去"的情况。
第一类是定期数据清理。最常见的例子是日志表、临时表、验证码表、登录Token表只保留最近N天数据。很多团队初期没接数据生命周期管理平台,手工delete又总是忘记,用事件调度器配上"每天凌晨3点删除30天前的操作日志"这种规则,一次建好长期生效,省心很多。
第二类是周期性的统计汇总。比如每小时统计一次订单量、每分钟计算一次在线人数、每天晚上生成一次销售日报表,这些都属于"把明细数据聚合成汇总数据"的场景。如果业务中有较高的查询性能需求,往往不适合每次都直接对明细表做聚合运算,而是用定时任务把结果提前算好写入汇总表,查询端直接读汇总表就行,响应速度会快很多。
第三类是状态自动流转。比如超过15分钟未支付的订单自动标记为已取消、超过72小时未确认收货的订单自动完成、定时把即将开始的活动状态从"未开始"改成"进行中",这些业务状态更新逻辑虽然也可以用业务代码在每次请求时判断,但用事件去"兜底扫描"更可靠,不会因为某次请求没触发而遗漏。
不过也要说清楚,事件调度器并不是万能的。如果你的任务涉及跨数据库操作、复杂的业务编排、需要调用外部接口,或者一次要处理的数据量极大、耗时很长,那仍然应该选择专门的任务调度系统。事件调度器适合的是"轻量、内聚、不依赖外部条件"的数据库自治任务,这个边界要心里有数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开启事件能力与核心语法细节
2.1 第一步永远是确认调度器开关
事件调度器虽然是MySQL自带的功能,但默认情况下并不一定会开启。MySQL从5.1.6版本开始引入这项功能,默认状态是OFF,有些发行版甚至会在编译时直接禁用。所以无论你是刚开始接触还是很久没用过,使用前都先确认一下当前实例的状态。
登录MySQL后执行下面这条命令,先看event_scheduler这个全局变量的值:
sql复制SHOW VARIABLES LIKE 'event_scheduler';
结果一般有三种取值,ON表示正在运行,OFF表示已关闭但可以动态打开,DISABLED表示在MySQL启动时就被禁用,这种情况没法在运行时直接修改,必须修改配置文件后重启实例。如果看到的是OFF,直接执行:
sql复制SET GLOBAL event_scheduler = ON;
这个操作不需要重启MySQL,对在线业务没有影响,但需要注意,这个设置只对当前实例生效,MySQL服务重启后会恢复原状。如果你希望长期保持开启,必须修改MySQL配置文件。在[mysqld]段下加入一行event_scheduler=ON,然后重启服务。
MySQL 8.0还提供了SET PERSIST命令,可以动态修改参数并将配置持久化到mysqld-auto.cnf文件中:
sql复制SET PERSIST event_scheduler = ON;
这个命令的好处是既不用手工改配置文件,又能保证重启后仍然生效,是我在8.0环境里比较推荐的方式。
提示:如果你执行SHOW PROCESSLIST,正常情况下能看到一个用户为event_scheduler的线程,Command列为Daemon,这就是事件调度线程。如果看不到这条记录,基本可以断定事件调度器没有处于运行状态,事件到了时间也不会执行。
2.2 两个全局权限直接决定你能否干活
使用事件功能涉及两个权限,一个管创建和修改,一个管开关。MySQL的授权体系里,EVENT权限控制用户能否创建、修改、删除事件,PROCESS权限则控制能否打开或关闭事件调度器。
给一个账号授事件权限的语法如下:
sql复制GRANT EVENT ON mydb.* TO 'appuser'@'localhost';
一般来说,业务账号只需要在某个库上有EVENT权限即可,不需要授全局权限。具体的授权粒度建议参考最小权限原则,事件归哪个库管,就授哪个库的事件权限,不要为了方便直接给ALL PRIVILEGES。
还有一个权限相关的隐含坑值得单独提醒:如果事件中的SQL操作涉及修改数据,那么执行事件的账号必须同时对目标表有INSERT、UPDATE、DELETE等对应权限。很多人在测试环境用root账号建了事件,换到生产环境用普通账号就发现事件一直不工作,查到最后往往都是普通账号对目标表的DML权限没给够,这类问题排查起来特别隐蔽。
3. 手写第一个事件,理解调度规则
3.1 从一个最简单的一次性事件入门
完整的事件创建语法比较长,一开始容易吓到人。其实剥开看,核心就是三个部分:什么时候执行、执行什么、事件状态是什么。下面这条语句创建一个最简单的一次性事件,它将在创建后1分钟执行一次清理动作:
sql复制CREATE EVENT evt_clean_old_log_once
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE
DO
DELETE FROM operation_log WHERE create_time < NOW() - INTERVAL 7 DAY;
这条语句中的AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE代表执行时间,DO后面跟的是要执行的SQL语句。也就是"从当前时间算起再过1分钟,自动去删除7天前的操作日志"。执行完这一次之后,如果没有设置ON COMPLETITION PRESERVE,事件会在执行完毕后自动删除。这种一次性事件很适合用来做"延迟执行一次"的任务,比如上线数据订正脚本时希望给业务一个缓冲时间,就可以创建这样一个事件。
上面每条语句其实都已经在MySQL客户端里可以直接跑通,但有一个新手最容易踩的点:在mysql命令行或者Navicat里写完多行SQL,一定要记得用DELIMITER处理存储过程的场景。如果DO语句块里是单条SQL,没有分号冲突问题,但如果DO后面跟的是BEGIN...END多语句块,就必须要先改分隔符,否则客户端会把语句截断报错。
3.2 周期性事件与时间参数组合
了解了AT一次性调度之后,再看周期性的EVERY子句就会轻松很多。实际业务中用的最多的也是周期性事件,它的基本结构如下:
sql复制CREATE EVENT evt_clean_old_log_daily
ON SCHEDULE EVERY 1 DAY
STARTS '2025-01-01 03:00:00'
DO
DELETE FROM operation_log WHERE create_time < NOW() - INTERVAL 7 DAY;
这条语句表示每天凌晨3点执行一次清理。这里有两个非常关键的点需要特别说明,也是我见过翻车最多的地方。
第一个点是EVERY 1 DAY如果不写STARTS,默认是从事件创建成功那一刻开始,往后每24小时执行一次。比如你在上午11点创建了每天执行的事件,它不会等到当天凌晨,而是第二天上午11点才第一次执行,之后每天上午11点执行。很多人建完事件之后第二天早上发现数据没被清理,其实就是这个原因。如果你想要每天固定时间执行,必须用STARTS显式指定开始时间。上面例子中指定了2025-01-01 03:00:00,事件就会在每天03:00:00执行,这里STARTS只是设定第一次执行的时间点,后续周期是在这个时间点的基础上累加。
第二个点是时间间隔的类型可以很丰富。MySQL支持YEAR、QUARTER、MONTH、DAY、HOUR、MINUTE、SECOND、WEEK等常用单位,还支持一些复合写法,比如EVERY 2 HOUR表示每2小时,EVERY 15 MINUTE表示每15分钟。EVERY 3 DAY这种写法也很直观。对于需要精确控制工作时间的任务,还可以在EVERY后继续加STARTS和ENDS来限定有效期范围,比如只在每年双11大促期间每半小时执行一次库存同步:
sql复制CREATE EVENT evt_sync_stock_promotion
ON SCHEDULE EVERY 30 MINUTE
STARTS '2025-11-10 20:00:00'
ENDS '2025-11-12 23:59:59'
DO
CALL sync_stock_procedure();
用STARTS和ENDS把事件限定在一个时间窗口内,大促结束后事件虽然还在,但已经不会继续触发。这样做的好处是明年如果还要用同一个事件,只需要修改STARTS和ENDS的时间参数,不需要重新创建。
3.3 用BEGIN...END块写更复杂的逻辑
如果每次执行只需要一条SQL,DO直接跟SQL就行。但实际场景里一个定时任务往往有多个步骤,这时就需要用BEGIN...END把多条语句包起来,并配合DELIMITER在客户端工具里正确执行。比如下面这个例子,每10分钟更新一次用户活跃状态,同时把流失用户写入单独的记录表:
sql复制DELIMITER $$
CREATE EVENT evt_user_active_archiver
ON SCHEDULE EVERY 10 MINUTE
DO
BEGIN
UPDATE user_profile
SET is_active = 0
WHERE last_login_time < NOW() - INTERVAL 30 DAY
AND is_active = 1;
INSERT INTO user_loss_record (user_id, lost_time)
SELECT id, NOW()
FROM user_profile
WHERE last_login_time < NOW() - INTERVAL 30 DAY
AND is_active = 0
AND id NOT IN (SELECT user_id FROM user_loss_record WHERE lost_time > NOW() - INTERVAL 7 DAY);
END$$
DELIMITER ;
从第二个示例可以清晰看出,一旦需要在定时任务里做多个操作,比如"先更新状态再到另一张表里留存记录",单条SQL已经扛不住了,必须引入BEGIN...END代码块。DELIMITER的切换只看客户端工具,它并不是MySQL服务器端的语法,只是告诉客户端"遇到$$才算语句输入结束"。所有在Navicat、命令行、MySQL Workbench中创建复杂事件的人,十有八九都会遇到因为没处理分隔符而报语法错误的情况,这个细节非常基础但对新手又非常致命。
注意:在DO语句块中如果包含多条语句,一定要记得用DELIMITER命令切换分隔符。否则客户端会在第一个分号处就认为语句结束,导致提交到服务端的SQL不完整,报出语法错误。另外DO语句块中最好不要包含动态SQL,事件执行环境很难调试,出了问题只能从日志排查,保持逻辑简单直接最稳妥。
4. 事件的查看、修改、删除全流程管理
4.1 查看事件列表和定义
手头有多个事件之后,怎么管理它们就成了下一个问题。MySQL提供了两种查看事件的方式,一种是通过SHOW语句,另一种是查询information_schema库中的EVENTS表。
sql复制SHOW EVENTS;
这种方式适合快速浏览当前库中有哪些事件。它会显示事件名、创建者、事件类型(RECURRING或ONE TIME)、执行时间定义、状态等基本信息。但SHOW EVENTS默认只显示当前所在数据库的事件,如果事件分散在多个库中,需要先USE到对应库或加上条件查询。
更完整的信息要通过information_schema.EVENTS来查看:
sql复制SELECT EVENT_SCHEMA, EVENT_NAME, STATUS, EVENT_TYPE,
EXECUTE_AT, INTERVAL_VALUE, INTERVAL_FIELD,
STARTS, ENDS, LAST_EXECUTED
FROM information_schema.EVENTS
WHERE EVENT_SCHEMA = 'mydb'\G
EVENTS表里能查到LAST_EXECUTED,也就是上一次实际执行的时间,这个字段对于排查"事件到底有没有执行"至关重要。如果发现时间到了但LAST_EXECUTED还是NULL,或者上一次执行时间停在很久以前,说明事件没有正常触发,需要继续往下排查。
查看某个事件的完整定义,可以用SHOW CREATE EVENT:
sql复制SHOW CREATE EVENT mydb.evt_clean_old_log_daily\G
它的输出和SHOW CREATE TABLE的格式类似,很直观地还原出当初创建用的整条语句,在你需要把事件迁移到别的环境时可以直接复制出来使用。
4.2 通过ALTER EVENT调整而不重建
业务变化之后,事件的时间策略或者对应逻辑需要调整,这时不用把事件删掉重建,直接用ALTER EVENT修改即可。ALTER EVENT的支持范围很广,包括调整时间规则、修改事件内容、启用或禁用事件、修改注释等。
最常见的调整是修改周期和状态开关。比如原先是每1小时执行一次,现在希望改成每30分钟一次:
sql复制ALTER EVENT evt_user_active_archiver
ON SCHEDULE EVERY 30 MINUTE;
如果临时需要暂停某个事件,不需要删除定义,只需要禁用:
sql复制ALTER EVENT evt_user_active_archiver DISABLE;
恢复的时候再执行:
sql复制ALTER EVENT evt_user_active_archiver ENABLE;
禁用事件和删除事件在运维上意义完全不同。禁用只是不触发,但事件定义仍然存在于数据库中,随时可以恢复;删除则是彻底移除定义,恢复必须重新创建。在线上环境遇到可疑事件时,我的习惯是先禁用观察一段时间,确认业务没有依赖后,再决定是否删除,这个习惯帮我避免过好多次误删事故。
4.3 事件过期处理策略要提前想好
使用ON COMPLETITION这个关键字可以控制一次性事件执行完之后是否保留。如果不写,默认是NOT PRESERVE,也就是事件执行完自动删除。如果写的是ON COMPLETION PRESERVE,执行完后事件会转为Disabled状态,但定义仍然保留在数据库里。
这个设置需要对业务场景有清晰预判。如果你创建的是一个只执行一次的"数据订正"任务,建议保留默认行为,执行完自动消失,不污染事件列表;如果你希望把某个事件当成一个可重复开关的开关——比如每年大促手动启用一次——那就可以加上ON COMPLETION PRESERVE,这样即使执行完了,也可以随时ALTER EVENT ENABLE再次启用。
另一个和过期处理相关的是事件生命周期管理。长期运行的MySQL实例上,事件列表可能会积累很多已经不再需要的事件,这些事件会一直占用系统表资源,而且可能因为绑定的表结构变更而在执行时报错。建议定期筛选一遍所有事件,把已经没有存在价值的旧事件清理掉,DBA上手第一件事就是这个。
5. 实操记录:一套完整的事件定时清理方案
5.1 建基础表模拟真实场景
讲了这么多理论,接下来我以一个实际场景完整演示一遍从建事件到验证结果的流程。假设现在有一张用户行为日志表user_behavior_log,数据量每天新增约50万行,业务方要求保留30天数据,超过30天的历史数据需要每天清理。我模拟一个简化版环境来操作。
首先创建一张日志表和一张清理记录表:
sql复制CREATE TABLE user_behavior_log (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
action VARCHAR(50) NOT NULL,
create_time DATETIME NOT NULL,
KEY idx_create_time (create_time)
) ENGINE=InnoDB;
CREATE TABLE event_execute_log (
id INT PRIMARY KEY AUTO_INCREMENT,
event_name VARCHAR(100) NOT NULL,
execute_time DATETIME NOT NULL,
affected_rows INT NOT NULL
) ENGINE=InnoDB;
然后插入几条测试数据,一条是30天前的老数据,一条是最近的数据:
sql复制INSERT INTO user_behavior_log (user_id, action, create_time) VALUES
(1001, 'login', NOW() - INTERVAL 31 DAY),
(1002, 'view', NOW() - INTERVAL 35 DAY),
(1003, 'click', NOW());
这样后续执行完删除事件后,可以一眼看出哪些行要被清理掉。
5.2 写事件包含删除+审计日志两条逻辑
设计需求是每天凌晨2点执行一次:删除超过30天的日志,并且把删除行数记录到event_execute_log表中,方便日后核对清理任务是否正常。如果用普通的两条SQL会非常零散,这里用BEGIN...END块组织:
sql复制DELIMITER $$
CREATE EVENT evt_clean_behavior_log_daily
ON SCHEDULE EVERY 1 DAY
STARTS '2025-01-01 02:00:00'
ON COMPLETION PRESERVE
DO
BEGIN
DECLARE deleted_count INT;
DELETE FROM user_behavior_log
WHERE create_time < NOW() - INTERVAL 30 DAY;
SET deleted_count = ROW_COUNT();
INSERT INTO event_execute_log (event_name, execute_time, affected_rows)
VALUES ('evt_clean_behavior_log_daily', NOW(), deleted_count);
END$$
DELIMITER ;
上面这个例子完整使用了DELIMITER切换、BEGIN...END块、局部变量、ROW_COUNT()函数。局部变量deleted_count先接收DELETE语句影响的行数,再把行数写入审计表。有了审计表,每次事件执行都有迹可循,这是生产环境运维中很推荐的做法,下次你怀疑"事件到底跑了没有"的时候,只需要一条SELECT就能确认,而不是到处翻日志。
设置ON COMPLETITION PRESERVE是因为我希望这个事件执行完之后定义留着,不要因为意外触发而丢失,每天定期任务本来就该长期保留。STARTS指定凌晨2点,是为了避开业务高峰期,批量删除大表数据时不会影响白天正常查询。
5.3 事件创建后的验证方法
事件创建完成之后,立刻想知道它有没有生效,建议分三步验证。第一步确认调度器线程与事件状态:
sql复制SHOW PROCESSLIST;
SHOW EVENTS\G
SHOW PROCESSLIST能看到event_scheduler线程,SHOW EVENTS能看到事件的Status为ENABLED。第二步,查看事件定义确保没有拼写问题:
sql复制SHOW CREATE EVENT mydb.evt_clean_behavior_log_daily\G
第三步,也是最有效的一步:手动触发一次事件逻辑。事件在MySQL里没有类似"手动立即执行"的命令,最直接的方式是用事件里相同的SQL自己跑一遍,确认SQL本身正确,或者临时创建一个短周期事件来验证时间触发机制是否正常。比如改成1分钟后执行一次,看能否在审计表里查到记录:
sql复制DROP EVENT IF EXISTS evt_test_trigger;
CREATE EVENT evt_test_trigger
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE
DO
INSERT INTO event_execute_log (event_name, execute_time, affected_rows)
VALUES ('evt_test_trigger', NOW(), 0);
等1分钟后查询event_execute_log表,如果出现了这条记录,就说明事件调度器整体链路是通的。如果链路不通,说明问题出在事件本身的逻辑或者权限上。
注意:真实事件里对核心业务表做DELETE,尤其像每天删除几十万行这种操作,建议不要只依赖事件本身的逻辑,而是配合pt-archiver这类工具分批删除,或者至少采用LIMIT分批+循环的方式,避免一次性产生超大事务,导致主从延迟飙升。事件语法本身能承载复杂的存储过程调用,所以生产环境的复杂清理逻辑更推荐把核心处理放到存储过程里,事件只负责定时调用存储过程,逻辑更加清晰。
6. 事件运维实战:状态、时区、权限排查
6.1 事件无法执行的六大原因排查表
事件功能本身不难,真正难的是出了问题时快速定位。我把自己在运维中遇到过的失败情况整理成一个排查清单,每次有问题对照着过一遍,基本几十秒内能锁定根源。
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 事件完全没有执行 | event_scheduler处于OFF或DISABLED | SHOW VARIABLES LIKE 'event_scheduler' |
| 时间到了但LAST_EXECUTED为空 | 事件被DISABLE | SHOW EVENTS查看Status列 |
| 事件执行时报错,但业务没感知 | SQL语句本身有问题,如表已删除 | 查看错误日志,在DO逻辑中增加审计表 |
| 权限足够但事件跨库操作失败 | 创建者账号对目标库表没有DML权限 | 使用root账号做对比测试 |
| 事件不按预期时间执行 | 时区设置不一致或STARTS指定错误 | 检查time_zone和STARTS的值 |
| 主从环境从库也执行了事件 | 没有设置DISABLE ON SLAVE | 检查事件定义,添加该状态 |
6.2 时区与主从复制:两个高频深坑
时区是一个特别值得单独拿出来说的问题。MySQL事件执行的时间判断受time_zone参数影响。如果服务器time_zone设置的是SYSTEM(跟随操作系统),而操作系统是UTC时区,那么事件中所有基于CURRENT_TIMESTAMP、NOW()、INTERVAL的计算都会按照UTC时间执行,和业务所在的北京时间就差了8个小时。"每天凌晨2点清理数据"的事件,实际可能会在北京时间上午10点触发。这是很多团队环境迁移后事件时间错乱的常见原因。
官方推荐的方案是把MySQL全局时区固定为具体时区,比如:
sql复制SET GLOBAL time_zone = '+08:00';
配置文件中也建议写入default-time-zone = '+08:00'。另外还要注意,事件中如果使用了DATETIME类型和TIMESTAMP类型,时区的影响范围也不同。DATETIME不随会话时区变化,它存什么就是什么;TIMESTAMP在写入和读取时会根据会话时区做转换。在写事件的时间比较条件时,建议尽量显式指定时间,少依赖隐式的时区转换逻辑。
另一个高频坑发生在主从复制架构中。事件调度器默认在所有实例上都会执行,如果主库上建了事件,从库通过复制拿到的是事件产生后的数据变更,而不是事件本身,按理说不会重复执行。但如果你在从库上也手动创建了同名事件,就会出现主从两边各跑一次的情况,最终数据要么重复处理,要么产生主键冲突。
针对这个场景,MySQL提供了DISABLE ON SLAVE选项。创建事件时指定这个状态,事件在从库上会保持禁用状态,不会被触发。但需要注意的是,它的判断依据是服务器是否开启了log_slave_updates以及当前实例是否被识别为从库。更保险的做法是只在主库创建事件,从库一律不建。如果非要在从库上保留事件定义来应对主从切换,那请务必使用DISABLE ON SLAVE,避免切换前这段时间从库自己执行任务产生脏数据。
6.3 事件是否真的执行了:三种追踪手段
判断事件是否执行,除了看业务表数据变化,最直接的方法就是查询information_schema.EVENTS里的LAST_EXECUTED字段:
sql复制SELECT EVENT_NAME, STATUS, LAST_EXECUTED
FROM information_schema.EVENTS
WHERE EVENT_SCHEMA = 'mydb';
LAST_EXECUTED有值并且时间符合预期,说明事件确实触发过。如果这个字段一直为NULL,但状态又是ENABLED,那问题大概率出在调度器本身或时间还没到。如果想看每一次执行的具体情况,最快的方式是打开通用日志:
sql复制SET GLOBAL general_log = 'ON';
SET GLOBAL log_output = 'TABLE';
开启之后,所有执行的SQL都会记录到mysql.general_log表中,包括事件调度器自动执行的语句。排查完记得及时关闭,因为general_log对性能影响很大,不适合长期打开。
如果你用的是MySQL 5.7及以上版本,还有更精细的方案,就是查询performance_schema的事件统计表:
sql复制SELECT EVENT_NAME, SQL_TEXT, TIMER_START, TIMER_END
FROM performance_schema.events_statements_history_long
WHERE EVENT_NAME LIKE '%statement%'
ORDER BY TIMER_START DESC
LIMIT 20;
审计表写入我这里格外推荐,这也是我在实际项目中坚持的实践。每当创建重要事件,就同时建一个审计表或至少让事件体里包含日志写入逻辑。这样无论是排查问题还是月底出报告,都有据可查。
7. 从一次生产事故看事件使用的边界
最后分享一个我真实踩过的教训。有段时间线上订单表的待支付数据总是不能被及时关闭,排查到最后发现是订单模块的定时"关单事件"在某个凌晨触发了死锁。原因是事件执行时间正好和一批业务批处理脚本撞在一起,两边按照不同的索引顺序更新同一批订单数据,形成了锁等待,最后事件里的UPDATE事务被InnoDB判定为死锁牺牲者回滚了。
那次事故让我彻底改变了对事件调度器的使用观念。现在我在设计事件时一定会考虑几个边界条件。
第一,单次执行的数据量必须设上限,大批量UPDATE/DELETE尽量拆成小批次循环提交,降低锁竞争和回滚代价。第二,关键事件的执行时间要尽量和业务高峰错开,如果没法完全错开,至少要在代码里做好冲突检测和重试机制。第三,事件逻辑不要做得太重,事件本身适合轻量任务,超过几百毫秒的逻辑就应该考虑拆出去交给独立的任务系统处理,不要把订单状态机那种核心流程的兜底策略完全寄托在事件上。
另外还有一点经验,新建事件之前建议先确认实例的负载和主从延迟情况。之前我见过有人在高负载的实例上建了一个每小时全表扫描的统计事件,结果每小时的整点都会把数据库IO打满一轮,导致业务查询出现明显抖动。如果任务逻辑一定需要跑全表,至少要在SQL里加好索引条件,并且选在业务低峰期执行。
事件调度器是个可靠的功能,但它和所有自动机制一样,都需要敬畏边界、做好监控。保留审计、记录LAST_EXECUTED、配置告警,这三件事做到位,事件调度器才会真正成为一个能让你"睡个好觉"的自动化利器。
