ALTER EVENT 能干三件事:改状态、改时间表、改事件体,改完都不用重启。
停一段时间再打开,欠下的点会一次补上——这是复刻版的行为,也是最容易踩的坑。
只改周期字段,下次时间不会自己重算;这类「派生状态没跟着更新」的坑很常见。
这一篇讲 ALTER EVENT。
教材 17.3 的重点是「改完不用重启」,这一篇补的是「改完哪些东西会跟着变、哪些不会」。
和第 14 章 ALTER PROCEDURE 对照:过程只能改「特性」(比如注释、权限),
要改过程体得先删再建;事件的 ALTER 能直接换事件体。
这个差别不小——事件的「在线改」是真的在线改。
一、ENABLE 与 DISABLE:最常用的一种改
最常用的一种 ALTER 不是改逻辑,是开关:临时停掉、临时打开。
ALTER EVENT e_log DISABLE; -- 停掉
ALTER EVENT e_log ENABLE; -- 恢复
ALTER EVENT e_log DISABLE ON SLAVE; -- 只在从库上停
后两种写法在沙箱里连语法都不认;复刻版只做前两件。
注意 DISABLE ON SLAVE 这个写法:它是给主从复制用的,意思是「这个事件在从库上不跑」。
为什么需要这个?因为事件在主库上执行,产生的数据会通过 binlog 同步到从库。
如果从库也自己跑一遍,同一件事就做了两次。
所以主从架构里,从库上的事件默认就该是「不执行」的。
停掉之后,下次时间还停在老位置没动。
这一点下一篇会用到:正因为没动,重新打开时才会把欠下的一起补上。
二、停一下再开:欠下的会一次补上
停着的时候一个都不跑,但下次时间还停在 00:01 没动。 这时候把它打开,再推一次,会发生什么?
| 时刻 | 状态 | tick 到 00:05 的结果 |
|---|---|---|
刚 DISABLE |
DISABLE |
没有事件被执行 |
ENABLE 之后再推 |
ENABLE |
跑 5 次(00:01 到 00:05) |
一次跑出了五个点:00:01 到 00:05。 这就是补跑的副作用:停了一段时间再打开,它会把停着期间欠下的全补上。 如果这个事件是「每小时统计一次」,这一下会连跑好几遍,把统计表算重。 所以「停一下再开」在真实系统里要小心: 要么连同「下次时间」一起重置,要么在事件体里先判断「这次该不该跑」。 MySQL 那边因为默认不补跑,这个问题反而没那么尖锐。 但「不补」也有它的问题:真该补的数据就那么漏了。 两边各缺一半,所以结论是一样的——别指望调度器,兜底自己写。
三、改周期:下次时间不会自己重算
这是复刻版里最容易踩的一个坑。
只改 every 字段,next 还停在老位置——等于「新周期从下下次才开始生效」。
| 做了什么 | 周期 | 下次时间 |
|---|---|---|
| 跑过一次之后 | 60 秒 | 11-01 00:02 |
| 只改字段 | 600 秒 | 11-01 00:02 |
走 alter(),重算 |
600 秒 | 11-01 00:11 |
第二行和第一行的下次时间一模一样——改了周期,节奏没变。
第三行是 alter() 里补的一步:改完周期把 next 重算成「现在 + 新周期」。
MySQL 那边是 ALTER EVENT ... ON SCHEDULE EVERY 2 HOUR,一口气重写整张时间表。
所以那边不存在这个问题——它是「重新定义时间表」,不是「改一个字段」。
凡是「只改一个属性」的接口,都要问一句「派生状态跟着更新了吗」。
这类坑在别处也常见:改了订单金额没重算税费、改了汇率没重算折算价、改了用户等级没重算权限。
共同点都是「改了源头、忘了下游」。
四、改事件体:下一轮自动生效
改事件体和改周期是两件事,分开做更安全。 复刻版里事件体就是一个函数,换掉它就行。
| 版本 | 执行记录 |
|---|---|
| v1 | v1@11-01 00:01 |
| v2 | v2@11-01 00:02 |
改完之后下一次跑的就是新版本,没有任何「重启」动作。
这一点和存储过程不一样:过程改了要重新 CALL,事件改了下一轮自动生效。
但它也意味着「改事件体」是一次高风险操作——下一条到点就执行,没有灰度、没有回滚。
所以改之前最好先 DISABLE,改完自己验一遍,再 ENABLE。
这三步的成本很低,但能挡掉大部分「上线即事故」。
尤其是那种「跑一次就把旧数据删掉」的事件——改错了没有第二次机会。
五、改名:没有专门的语句,只能重建
MySQL 支持 ALTER EVENT e_old RENAME TO e_new;
沙箱连 EVENT 这个词都不认,复刻版也没做这一步。
改名只能靠 drop 加 create 两步拼出来。
| 步骤 | 库里剩下什么 |
|---|---|
| 改名之前 | e_new |
drop 加 create |
e_new |
第二行只有一个 e_new,e_old 没了、e_new 有了——但中间有个空档。
如果这个空档正好跨过一个执行点,那一次就丢了。
所以改名要挑时间:要么在两次执行之间,要么先把状态停掉再改。
停掉再改这个做法最稳:DISABLE、drop、create、ENABLE,四步,空档期不会误跑。
这条经验可以推广:凡是「没有原子改名」的系统,改名都是「删加建」。
而「删加建」中间的空档,永远是丢东西的地方——所以要挑低峰、要停服务、要验结果。
六、动手 Demo:停、开、改周期、换事件体、改名,五步走一遍
这一段把 ALTER EVENT 的五种用法挨个跑:停掉看它不跑、打开看它补跑、只改周期字段看下次时间不动、
走 alter() 看重算、换事件体看下一轮生效,最后用 drop 加 create 模拟改名。
BASE = datetime.datetime(2026, 11, 1, 0, 0, 0)
s = Sched(BASE)
s.create("e_log", every=60, body=lambda t: None)
s.tick(BASE + datetime.timedelta(minutes=1)) # 先跑一次,next 到 00:02
# ① 停掉:一个都不跑,下次时间不动
s.alter("e_log", status="DISABLE")
print(s.tick(BASE + datetime.timedelta(minutes=5))) # []
print(s.ev["e_log"]["next"]) # 仍是 00:02
# ② 打开:欠下的全补上
s.alter("e_log", status="ENABLE")
print(len(s.tick(BASE + datetime.timedelta(minutes=5)))) # 4
# ③ 只改周期字段:下次时间不重算
before = s.ev["e_log"]["next"]
s.ev["e_log"]["every"] = 600
print(before == s.ev["e_log"]["next"]) # True,没变
# ④ 走 alter():改完周期把 next 重算成「现在 + 新周期」
s.alter("e_log", every=600)
print(s.ev["e_log"]["next"]) # 现在 + 10 分钟
# ⑤ 换事件体:下一轮就是新版本
s.ev["e_log"]["body"] = lambda t: print("v2", t)
s.tick(BASE + datetime.timedelta(minutes=20))
# ⑥ 改名:没有 RENAME,只能删了重建
s.drop("e_log")
s.create("e_log_v2", every=600)
print(sorted(s.ev)) # ['e_log_v2']
实跑输出:
--------------------------------------------------------------------
修改事件:改状态、改周期、改事件体
--------------------------------------------------------------------
ENABLE / DISABLE:最常用的一种改
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER EVENT e_log DISABLE; -- 停掉
| ALTER EVENT e_log ENABLE; -- 恢复
| ALTER EVENT e_log DISABLE ON SLAVE; -- 只在从库上停
后两种写法在沙箱里连语法都不认;复刻版只做前两件。
时刻 状态 tick 到 00:05 的结果
-------------------------------------------
刚 `DISABLE` DISABLE 没有事件被执行
**停着的时候一个都不跑,但下次时间还停在 00:01 没动。**
**这时候把它打开,再推一次,会发生什么?**
打开后 tick 到 00:05 -> e_log@11-01 00:01、e_log@11-01 00:02、e_log@11-01 00:03、
e_log@11-01 00:04、e_log@11-01 00:05
**一次跑出了五个点:00:01 到 00:05。**
**这就是补跑的副作用:停了一段时间再打开,它会把停着期间欠下的全补上。**
**如果这个事件是「每小时统计一次」,这一下会连跑好几遍,把统计表算重。**
**所以「停一下再开」在真实系统里要小心:**
**要么连同「下次时间」一起重置,要么在事件体里先判断「这次该不该跑」。**
改周期:下次时间不会自己重算
--------------------------------------------------------------------
**这是复刻版里最容易踩的一个坑。**
**只改 `every` 字段,`next` 还停在老位置——等于「新周期从下下次才开始生效」。**
做了什么 周期 下次时间
---------------------------------------
跑过一次之后 60 秒 11-01 00:02
只改字段 600 秒 11-01 00:02
走 `alter()`,重算 600 秒 11-01 00:11
**第二行和第一行的下次时间一模一样——改了周期,节奏没变。**
**第三行是 `alter()` 里补的一步:改完周期把 `next` 重算成「现在 + 新周期」。**
**MySQL 那边是 `ALTER EVENT ... ON SCHEDULE EVERY 2 HOUR`,一口气重写整张时间表。**
**所以那边不存在这个问题——它是「重新定义时间表」,不是「改一个字段」。**
**凡是「只改一个属性」的接口,都要问一句「派生状态跟着更新了吗」。**
改事件体:换一段 SQL
--------------------------------------------------------------------
**改事件体和改周期是两件事,分开做更安全。**
**复刻版里事件体就是一个函数,换掉它就行。**
**这也顺带说明一件事:事件体里能放什么,取决于「谁来执行」。**
版本 执行记录
--------------------
v1 v1@11-01 00:01
v2 v2@11-01 00:02
**改完之后下一次跑的就是新版本,没有任何「重启」动作。**
**这一点和存储过程不一样:过程改了要重新 `CALL`,事件改了下一轮自动生效。**
**但它也意味着「改事件体」是一次高风险操作——下一条到点就执行,没有灰度、没有回滚。**
改名:没有专门的语句,只能重建
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER EVENT e_old RENAME TO e_new;
MySQL 支持 RENAME TO;沙箱连 EVENT 这个词都不认,复刻版也没做这一步。
步骤 库里剩下什么
-------------------------------
改名之前 e_new
`drop` + `create` e_new
**第二行只有一个 `e_new`,`e_old` 没了、`e_new` 有了——但中间有个空档。**
**如果这个空档正好跨过一个执行点,那一次就丢了。**
**所以改名要挑时间:要么在两次执行之间,要么先把状态停掉再改。**
第①步那个 [] 和第②步那个 4 是一对:停着不跑、开了补跑。
这两个数放在一起才是完整的行为,光看一个会得出相反的结论。
第③步那个 True 是整段最值得记的一行:改了周期,下次时间居然没变。
它不报错、不告警,就是一个安静的不一致。
这类「派生状态没更新」的 bug,只能靠「改完立刻读一遍」发现。
最后一步只剩 e_log_v2 一个——中间那个空档是真实的,只不过这次没跨过执行点,所以没丢东西。
没丢不等于不会丢:换个时间点跑,结论就可能不一样。
30 秒实验
- 把第②步的 tick 目标从 00:05 改成 01:00,看补跑几次, 再想想「每小时一次的事件停了一天再打开」会发生什么。
- 在第③步之后立刻
tick到 00:12,看它是 00:12 跑还是 00:12 之后才跑, 解释「新周期从下下次生效」这句话具体体现在哪。 - 把第⑥步的
drop和create之间插一次tick,看会不会报「事件不存在」。
常见误区
- 以为
ALTER EVENT ... ENABLE是「从当前时刻重新开始」。下次时间没被重置, 所以停着期间欠下的点会被一次补上。 - 以为改了
EVERY之后节奏立刻变。只改周期字段时下次时间不动,新节奏从下下次才开始体现。 - 以为改名是一条语句的事。没有
RENAME就只能删了重建,中间那段空档里事件是不存在的。
自测三问(点开对答案)
1. 事件 `DISABLE` 之后,下次时间会变吗? —— 不会,它还停在原来的位置。正因为没动,重新 `ENABLE` 之后才会把欠下的点一次补上。 要「从当前时刻重新开始」,得手动重置下次时间。 2. 为什么改周期之后要重算下次时间? —— 因为下次时间是「按老周期算出来的」派生值。 只改周期不改它,新周期要等下一次执行完才生效——中间那一次还按老节奏走。 3. 给事件改名的安全做法是什么? —— 先 `DISABLE`,再 `DROP`,再 `CREATE` 新名字,最后 `ENABLE`。 这样空档期事件是停着的,不会出现「正好在空档里该跑却没跑」。修改讲完了。下一篇是第 17 章最后一篇:删除事件,以及删完怎么验。
- ☐ (本篇完,下一篇 17.4 删除事件)