RSS
菜单
全部文章快讯开发科技深度热点

第 57 章 事件调度踩坑

内容摘要

MySQL 的 Event Scheduler(事件调度器)相当于数据库自带的 crontab,适合做定时统计、定期清理、数据归档。但它"藏在数据库里"的特性也带来一堆坑:没开调度器、时区不对、从库重复跑、执行失败无日志等。本章梳理事件调度使用中的 11 个高频踩坑。示例基于 MySQL 8.0(本机无服务,语法按官方文档书写)。读完你能安全地用 Event 做定时任务。

📖 本篇简介:MySQL 的 Event Scheduler(事件调度器)相当于数据库自带的 crontab,适合做定时统计、定期清理、数据归档。但它"藏在数据库里"的特性也带来一堆坑:没开调度器、时区不对、从库重复跑、执行失败无日志等。本章梳理事件调度使用中的 11 个高频踩坑。示例基于 MySQL 8.0(本机无服务,语法按官方文档书写)。读完你能安全地用 Event 做定时任务。

目录

  • 坑 1:event_scheduler 没开事件不执行
  • 坑 2:单次事件跑完就消失
  • 坑 3:事件时间用服务器时区不对
  • 坑 4:事件执行失败没日志看不到
  • 坑 5:多个事件同时跑互相锁表
  • 坑 6:以为事件和 crontab 一样是应用层
  • 坑 7:EVERY 周期从创建时刻起算
  • 坑 8:事件里长事务阻塞调度
  • 坑 9:DEFINER 失效事件不跑
  • 坑 10:主从复制上从库又跑一遍事件
  • 坑 11:临时禁用事件忘了重新开启
  • 速查表:事件调度速记
  • 注意事项:用事件前的 Checklist
  • 自测:你会用 Event 吗

坑 1:event_scheduler 没开事件不执行

场景 / 错误示范:

-- 创建了事件,到点却不执行,开发者以为写错了语法
CREATE EVENT ev_clean
ON SCHEDULE EVERY 1 HOUR
DO DELETE FROM log_t WHERE create_time < NOW() - INTERVAL 7 DAY;

-- 实际上:调度器根本没开
SHOW VARIABLES LIKE 'event_scheduler';   -- 结果 OFF

为什么是坑:MySQL 的事件调度器默认是关闭的(OFF)。建了事件但调度器没开,事件永远不会被触发,且没有任何报错。这是用 Event 的第一个必踩坑。

正确做法:

-- 开启调度器(运行时)
SET GLOBAL event_scheduler = ON;
SHOW VARIABLES LIKE 'event_scheduler';   -- 确认 ON

-- 永久生效要写进配置文件 my.cnf
-- [mysqld]
-- event_scheduler=ON
-- 确认事件调度线程状态
SHOW PROCESSLIST;   -- 能看到一条 "Daemon" 状态为 "Waiting for next activation" 的线程
-- 看到这条线程,才说明调度器真的起来了
-- 没看到 = event_scheduler 还是 OFF

加分点:事件调度器是一个专门的后台线程,ON 才会起线程去检查到点的事件。新装库第一件事就是确认它开了。

坑 2:单次事件跑完就消失

场景 / 错误示范:

-- 想"每天凌晨跑一次",结果事件只执行了一次就没了
CREATE EVENT ev_once
ON SCHEDULE AT '2026-10-08 02:00:00'
DO CALL daily_job();
-- 这是单次事件(AT),执行完默认就被丢弃

为什么是坑:AT 是单次事件,EVERY 才是循环事件。而且循环事件默认 ON COMPLETION NOT PRESERVE,执行完(或单次)后会被自动删除。开发者以为建了就一直跑。

正确做法:

-- 循环任务用 EVERY,并显式 ON COMPLETION PRESERVE 保留
CREATE EVENT ev_daily
ON SCHEDULE EVERY 1 DAY
STARTS '2026-10-08 02:00:00'
ON COMPLETION PRESERVE          -- 执行后保留,别自动删
DO CALL daily_job();

-- 查看所有事件状态
SELECT event_name, status FROM information_schema.events;
-- 事件状态一览:status=ENABLED 才会跑,DISABLED 不跑
SELECT event_name, event_definition, status, last_executed
FROM information_schema.events
WHERE event_schema = 'your_db';
-- last_executed 字段直接告诉你它上次什么时候跑的,没跑过就是 NULL

加分点:循环任务一定 EVERY + ON COMPLETION PRESERVE;单次任务才用 AT。建完去 information_schema.events 确认 status=ENABLED。

坑 3:事件时间用服务器时区不对

场景 / 错误示范:

-- 事件定在"凌晨 2 点",结果按数据库服务器时区跑,比预期早了 8 小时
CREATE EVENT ev_daily
ON SCHEDULE EVERY 1 DAY STARTS '2026-10-08 02:00:00' ...
-- 数据库服务器是 UTC,02:00 UTC = 北京时间 10:00,不是凌晨

为什么是坑:事件调度用的是数据库服务器的时区(time_zone 系统变量),不是应用服务器、也不是业务认知的时区。跨时区部署、容器默认 UTC 时,事件触发时间和预期差几个小时。

正确做法:

-- 确认数据库时区
SELECT @@global.time_zone, @@session.time_zone;   -- SYSTEM 或具体值

-- 显式指定事件执行时基于哪个时区(或把库时区设成业务时区)
SET GLOBAL time_zone = '+8:00';
-- 之后 STARTS '02:00:00' 就是东八区凌晨 2 点

加分点:容器化部署 MySQL 默认 UTC,这是事件时间"莫名差 8 小时"的元凶。上线前务必核对 time_zone。

坑 4:事件执行失败没日志看不到

场景 / 错误示范:

-- 事件里 CALL 的存储过程报错了,但业务方完全不知道
CREATE EVENT ev_daily ON SCHEDULE EVERY 1 DAY DO CALL daily_job();
-- daily_job 里某条 SQL 报错,事件静默失败,没有任何告警

为什么是坑:事件执行失败默认只写进错误日志,应用层无感知。定时任务悄悄挂了,直到几天后业务对账才发现数据没更新。这是定时任务类系统的通病。

正确做法:

-- 在事件逻辑里自己记录执行结果:建一张事件运行日志表
CREATE TABLE ev_log(
    event_name VARCHAR(64), run_time DATETIME,
    status VARCHAR(20), msg VARCHAR(500)
);
-- 事件 DO 里包一层:成功/失败都记日志
-- DO CALL daily_job_with_log();   -- 过程内部写 ev_log
-- 再对 ev_log 做监控:连续 N 天失败就告警
-- 事件运行日志表示例
CREATE TABLE ev_log(
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_name VARCHAR(64),
    run_time DATETIME,
    status VARCHAR(20),          -- SUCCESS / FAIL
    msg VARCHAR(500)
);
-- 每天早上查一次:昨天该跑的事件跑成功了吗
SELECT event_name, COUNT(*) FROM ev_log
WHERE run_time > CURDATE() - INTERVAL 1 DAY
GROUP BY event_name;

加分点:定时任务必须有"执行留痕+失败告警"。靠 MySQL 错误日志不够,业务侧要能查到"这个任务今天跑没跑、成功没成功"。

坑 5:多个事件同时跑互相锁表

场景 / 错误示范:

-- 几个事件都定在凌晨 2 点,同时跑,都在操作同几张表
-- ev_a: 02:00 统计;ev_b: 02:00 清理;ev_c: 02:00 归档
-- 三个事件并发操作同几张表,互相锁等待,甚至死锁

为什么是坑:MySQL 默认每个事件触发时开一个新线程执行。多个事件同一时刻启动,又操作相同的表,就产生并发锁冲突。开发者把所有定时任务都堆在整点,等于自己跟自己抢锁。

正确做法:

-- 错峰:不同事件定在不同分钟
CREATE EVENT ev_a ON SCHEDULE EVERY 1 DAY STARTS '2026-10-08 02:00:00' ...;
CREATE EVENT ev_b ON SCHEDULE EVERY 1 DAY STARTS '2026-10-08 02:15:00' ...;
CREATE EVENT ev_c ON SCHEDULE EVERY 1 DAY STARTS '2026-10-08 02:30:00' ...;
-- 错开 15 分钟,避免并发锁表

加分点:定时任务排期像航班时刻——别全挤在一个点。错峰不仅避免锁冲突,也让数据库负载更平滑。

坑 6:以为事件和 crontab 一样是应用层

场景 / 错误示范:

-- 开发者:事件不就是数据库版 crontab 吗,随便建
-- 问题:事件跑在数据库服务器上,消耗的是数据库的 CPU/IO/连接

为什么是坑:事件在数据库内部执行,它的计算压力直接打在数据库上。把重计算、重查询的任务放进事件,等于让数据库既当存储又当计算,高峰期和业务抢资源。crontab 在应用/独立机器上跑,资源隔离。

正确做法:

-- 事件只做"轻量、数据库内"的定时事(如清理过期数据、简单汇总)
-- 重计算、批量导出、复杂统计,放应用层任务调度(XXL-JOB 等),别塞数据库
-- 判断标准:这个任务跑起来会不会和业务高峰抢 CPU/IO?会就别用事件

加分点:数据库事件适合轻量定时维护;重型数据处理交给独立调度系统,资源隔离、可监控、可重试。

坑 7:EVERY 周期从创建时刻起算

场景 / 错误示范:

-- 想要"每小时整点跑",结果从创建时刻(10:23)开始,每小时 23 分跑
CREATE EVENT ev_hourly ON SCHEDULE EVERY 1 HOUR DO CALL job();
-- 10:23 创建,下次是 11:23、12:23,不是整点

为什么是坑:EVERY 循环事件如果不显式指定 STARTS,周期从创建那一刻开始算。开发者以为是"整点",实际是"创建时间点"。要固定在某个时刻,必须显式 STARTS。

正确做法:

-- 用 STARTS 指定从哪个固定时刻开始
CREATE EVENT ev_hourly
ON SCHEDULE EVERY 1 HOUR
STARTS '2026-10-08 00:00:00'   -- 从零点开始,之后每小时整点跑
DO CALL job();

加分点:EVERY 事件一定要配 STARTS 指定起始点,否则触发时刻跟着创建时间漂移,运维时很难预期它什么时候跑。

坑 8:事件里长事务阻塞调度

场景 / 错误示范:

-- 事件里跑了一个大事务(归档千万行),跑了半小时
CREATE EVENT ev_arch ON SCHEDULE EVERY 1 DAY DO CALL archive_big();
-- 这个事件线程长时间持锁,期间相关表的业务写全被堵

为什么是坑:事件触发的 SQL 也是普通事务,跑慢了一样占锁、占资源。重归档任务放进事件,和坑 5、坑 6 叠加,凌晨把库锁死。

正确做法:

-- 事件里的任务也要分批小事务,别一个大事务跑到底
-- CALL 的存储过程内部按主键分批,每批独立提交
-- 大归档优先用专门的离线任务,别用数据库事件扛
-- 事件调用的归档过程内部要分批
-- CALL archive_proc();  -- 过程里:
-- DELETE FROM archive_log WHERE create_time < ? LIMIT 2000;
-- 循环多次,每批独立小事务,事件整体执行时间可控
-- 事件调度器线程一次只跑一个事件,别让它长时间占着

加分点:定时任务的 SQL 也要遵守"小事务、分批"原则,不能因为是定时跑就肆无忌惮。

坑 9:DEFINER 失效事件不跑

场景 / 错误示范:

-- 事件是 'root'@'%' 创建的,后来这个账号被删了/改密码了
-- 事件到点不执行,报 "definer does not exist" 类错误
CREATE DEFINER='root'@'%' EVENT ev_daily ...;

为什么是坑:事件和存储过程一样有 DEFINER,默认以创建者权限运行。创建者账号被删或权限回收后,事件执行时找不到合法 definer,直接失败。迁移数据库后尤其常见。

正确做法:

-- 查看事件的 definer
SELECT event_name, definer FROM information_schema.events;
-- 把 definer 改成存在的账号
ALTER DEFINER = 'event_runner'@'%' EVENT ev_daily ...;
-- 用一个专门的、不会轻易删除的低权限账号做事件 definer
-- 创建事件时就用专用账号做 definer
CREATE DEFINER='event_runner'@'%' EVENT ev_daily
ON SCHEDULE EVERY 1 DAY STARTS '2026-10-08 02:00:00'
DO CALL daily_job();
-- event_runner 只授执行该过程的最小权限,长期存在不删除
-- 迁移到新库时,这个账号要先建好,事件才能正常跑

加分点:建事件/过程时用专门的服务账号做 DEFINER,别用 root。账号一旦变动,事件静默失败。

坑 10:主从复制上从库又跑一遍事件

场景 / 错误示范:

-- 主库建了事件,binlog 把事件复制到从库
-- 从库的 event_scheduler 也开着,结果同一个事件在主从各跑一遍
-- 数据被重复处理(重复清理、重复统计翻倍)

为什么是坑:事件是会被复制的对象。如果从库也开了 event_scheduler,同一个事件就在主库、从库各执行一次,重复处理数据。这是主从架构下事件调度的经典坑。

正确做法:

-- 标准做法:从库关闭事件调度器,只让主库跑事件
-- my.cnf 从库配置:
-- event_scheduler=OFF
-- 主库跑事件,事件的 SQL 通过 binlog 复制到从库应用,数据自然同步
SHOW VARIABLES LIKE 'event_scheduler';   -- 从库应为 OFF
-- 验证从库是否真的关了调度器
SHOW VARIABLES LIKE 'event_scheduler';   -- 从库应为 OFF
-- 即使 binlog 把建事件语句复制过来了,从库不启动调度线程,事件也不会跑
-- 这正是我们要的:主库跑,从库只回放结果数据

加分点:主从架构里,事件只在主库执行,从库靠复制拿到结果。从库 event_scheduler 必须关,这是标准配置。

坑 11:临时禁用事件忘了重新开启

场景 / 错误示范:

-- 维护时临时禁用了事件,忙完忘了重新开
ALTER EVENT ev_daily DISABLE;
-- 几周后对账才发现,这段时间定时任务一直没跑

为什么是坑:DISABLE 一个事件很容易,但"重新 ENABLE"这个动作没人提醒。定时任务静默停摆,靠人工记忆恢复,极不可靠。

正确做法:

-- 禁用时记好待办;用监控兜底
ALTER EVENT ev_daily DISABLE;
-- 恢复:
ALTER EVENT ev_daily ENABLE;
-- 关键:ev_log 监控里加一条"事件应跑未跑"告警,停摆立刻发现

加分点:对"必须每天跑"的事件,监控"今天跑了没有"比"跑成功没有"更重要——停摆本身就是事故。

速查表:事件调度速记

| 坑点 | 错误做法 | 正确做法 |

|---|---|---|

| 调度器 | 建完就以为会跑 | 先开 event_scheduler=ON |

| 单次/循环 | 用 AT 当循环 | EVERY + ON COMPLETION PRESERVE |

| 时区 | 靠服务器默认 | 核对 time_zone,容器常是 UTC |

| 失败日志 | 静默失败 | 自建 ev_log + 告警 |

| 并发 | 全堆整点 | 错峰排期 |

| 资源 | 重计算塞事件 | 重活交独立调度 |

| 起始时刻 | 不显式 STARTS | STARTS 指定固定点 |

| 大事务 | 事件里跑大事务 | 分批小事务 |

| DEFINER | 用 root | 专门服务账号 |

| 主从 | 从库也开调度 | 从库关闭,主库跑 |

| 禁用恢复 | 靠记忆 | 监控"应跑未跑" |

注意事项:用事件前的 Checklist

  • [ ] event_scheduler 确认 ON 了吗?
  • [ ] 循环事件用了 EVERY + ON COMPLETION PRESERVE 吗?
  • [ ] 数据库 time_zone 和业务时区一致吗?
  • [ ] 事件执行有日志和失败告警吗?
  • [ ] 多个事件错峰了吗?
  • [ ] 任务是不是太重,该不该交独立调度?
  • [ ] 主从架构下,从库 event_scheduler 关了吗?
  • [ ] DEFINER 用的是不会删的服务账号吗?

一句话:事件调度三句话——调度器要开、主库跑从库关、执行留痕加告警。

自测:你会用 Event 吗

题 1:建了事件却到点不执行,最可能的原因?

<details>

<summary>参考答案</summary>

event_scheduler 默认 OFF,调度器线程没启动。SET GLOBAL event_scheduler=ON 并写进配置。

</details>

题 2:循环事件为什么要加 ON COMPLETION PRESERVE?

<details>

<summary>参考答案</summary>

默认 ON COMPLETION NOT PRESERVE,事件执行后会被自动删除。循环任务要 PRESERVE 保留,否则跑一次就没了。

</details>

题 3:容器里事件触发时间比预期早 8 小时,为什么?

<details>

<summary>参考答案</summary>

容器默认 UTC 时区,事件按数据库 time_zone 调度。把 time_zone 设成 +8:00 即可。

</details>

题 4:事件里的任务失败了,应用层怎么第一时间知道?

<details>

<summary>参考答案</summary>

靠 MySQL 错误日志不够。应自建 ev_log 表记录每次执行成功/失败,并对接监控做"连续失败"和"应跑未跑"告警。

</details>

题 5:多个事件都定在凌晨 2 点,有什么风险?

<details>

<summary>参考答案</summary>

并发操作同几张表互相锁等待甚至死锁。应错峰(不同分钟)排期,负载也更平滑。

</details>

题 6:为什么重计算任务不建议放数据库事件里?

<details>

<summary>参考答案</summary>

事件跑在数据库服务器上,直接消耗数据库 CPU/IO,和业务抢资源。重任务应放独立调度系统,资源隔离。

</details>

题 7:EVERY 1 HOUR 不显式 STARTS,触发时刻是什么样的?

<details>

<summary>参考答案</summary>

从创建时刻开始算,如 10:23 创建则每小时 23 分跑。要整点跑必须显式 STARTS 到固定时刻。

</details>

题 8:主从复制架构下,从库的 event_scheduler 该开吗?

<details>

<summary>参考答案</summary>

不该开。事件只在主库执行,结果通过 binlog 复制到从库。从库再开会导致同一事件重复跑、数据重复处理。

</details>

题 9:迁移数据库后事件不执行,报 definer 相关错误,为什么?

<details>

<summary>参考答案</summary>

事件的 DEFINER 账号在新库不存在了。要把 DEFINER ALTER 成新库存在的服务账号。

</details>

题 10:临时 DISABLE 一个事件后,靠什么保证它不会被忘?

<details>

<summary>参考答案</summary>

靠记忆不可靠。用监控"事件应跑未跑"告警,一旦停摆立刻发现;禁用动作本身记进待办/变更单。

</details>

题 11:怎么确认一个事件到底跑没跑过、上次什么时候跑的?

<details>

<summary>参考答案</summary>

查 information_schema.events 的 last_executed 字段,NULL 就是从没跑过。再配合自建 ev_log 表看每次执行结果和状态。

</details>

下一篇预告:第 58 章 数据导入导出踩坑

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 57 章 事件调度踩坑》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。