必须用BEFORE UPDATE触发器+SIGNAL SQLSTATE '45000'才能真正阻止非法更新;AFTER UPDATE数据已写入磁盘,无法拦截。BEFORE阶段可安全比对OLD/NEW值,校验不可变字段、状态机越界和数值合理性,并需注意NULL比较、权限覆盖与跨表查询等失效风险。

必须用 BEFORE UPDATE + SIGNAL,AFTER 什么都拦不住
BEFORE UPDATE 是唯一能真正中断非法更新的时机。AFTER UPDATE 触发时,NEW 值早已写入磁盘,此时 SIGNAL 只会让语句报错,但数据已改——相当于“报警时火已经烧完了”。BEFORE UPDATE 则在事务提交前最后一刻执行,可安全读取 OLD.column 和 NEW.column,做校验后用 SIGNAL SQLSTATE '45000' 强制中止整个语句。
判断字段是否被非法修改:别直接用 != 比较 NULL
MySQL 中 NULL != NULL 返回 NULL(不是 TRUE),所以 IF NEW.id != OLD.id 在两边都为 NULL 时不会触发。真实场景下必须拆开判断:
IF (NEW.id IS NULL) != (OLD.id IS NULL) OR NEW.id != OLD.id THEN ...- 或更简洁地用 MySQL 8.0.22+ 的
IS DISTINCT FROM:IF NEW.id IS DISTINCT FROM OLD.id THEN ... - 如果业务上“从非 NULL 改成 NULL”也算非法,仅靠
IS DISTINCT FROM就够;但如果只禁止“非 NULL → 非 NULL 修改”,就得额外加条件限定
常见拦截场景和对应写法
不是所有字段都要拦,重点防三类高危变更:
-
不可变字段:如
id、created_at、invoice_no。IF NEW.id IS DISTINCT FROM OLD.id THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'id is immutable'; -
状态机越界:如
OLD.status = 'paid'时,只允许NEW.status IN ('refunded', 'reversed'),否则SIGNAL -
数值合理性:比如余额变动不能为负数,或金额小数位不能超 2 位——需先转字符串:
CHAR_LENGTH(SUBSTRING_INDEX(CAST(NEW.amount AS CHAR), '.', -1)) > 2
最容易被忽略的失效点
触发器写得再严,以下情况也会让它形同虚设:
- 运维或旧服务用直连账号执行
UPDATE,而该账号没被限制权限——只要拥有表级UPDATE权限,就绕过所有应用层逻辑 -
DEFINER用户权限不足,导致触发器静默失效(查INFORMATION_SCHEMA.TRIGGERS确认STATUS = 'ENABLED') - 误在触发器里查其他表或调用函数,不仅拖慢所有
UPDATE,还可能引发死锁;MySQL 8.0 前也不支持触发器内事务控制 - 批量操作(如
LOAD DATA INFILE或大UPDATE ... LIMIT)会让触发器逐行执行,性能断崖下跌,上线前必须压测
真正难的从来不是写几行 SIGNAL,而是确保所有写入口——包括 DBA 脚本、ETL 工具、临时排查 SQL——全都经过这个触发器。只要漏掉一个直连路径,防护就归零。











