触发器里写 INSERT 日志语句为什么没生效
MySQL 触发器中执行
INSERT INTO log_table失败,最常见的原因是触发器和日志表在同一个事务中,而日志表用了
MyISAM引擎(不支持事务),或者用了
InnoDB但触发器本身因权限、SQL_MODE 或递归限制被静默终止。尤其注意:如果日志表是
InnoDB,且触发器在
AFTER UPDATE中写日志,而日志语句又触发了另一个触发器(比如日志表上也有
AFTER INSERT),就可能因
innodb_lock_wait_timeout或
max_sp_recursion_depth导致中断。
实操建议:
统一用InnoDB创建日志表,并显式关闭日志表的触发器(避免嵌套):
CREATE TABLE user_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, table_name VARCHAR(64), action VARCHAR(10), old_data JSON, new_data JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;在触发器开头加
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;防止日志失败拖垮主操作(仅适用于审计类日志,非关键业务流) 检查
SHOW VARIABLES LIKE 'log_bin_trust_function_creators';,若为
OFF且触发器含函数调用,需设为
ON或用
SET GLOBAL临时放开
BEFORE vs AFTER 触发器记录变更数据的区别
记录“改了什么”,关键看你想捕获的是变更前状态、变更后状态,还是两者都要。比如审计敏感字段(如
salary),
BEFORE UPDATE能拿到
OLD.salary,
AFTER UPDATE才能读到
NEW.salary;但
AFTER触发器不能修改
NEW值,而
BEFORE可以——这直接影响你是否能在日志里存脱敏值(如把手机号中间四位替换成
****)。
实操建议:
要记录完整变更对比,必须组合使用:BEFORE UPDATE存
OLD.*到临时用户变量(如
@old_salary := OLD.salary),再在
AFTER UPDATE中读取并插入日志 避免在
BEFORE INSERT中对
NEW.id赋值后,又在日志里记
NEW.id——此时自增 ID 尚未生成,会是
0或
NULL,应改用
AFTER INSERT+
LAST_INSERT_ID()
BEFORE DELETE是唯一能拿到被删行全量数据的时机,
AFTER DELETE中
OLD已不可访问
JSON 类型存日志字段时的兼容性陷阱
MySQL 5.7+ 支持
JSON类型,但触发器里直接拼
JSON_OBJECT('id', NEW.id, 'name', NEW.name) 很方便,问题在于:如果 NEW.name是
NULL,
JSON_OBJECT会忽略该键;如果字段含特殊字符(如换行、双引号),不加
JSON_QUOTE()会导致 JSON 格式损坏;更隐蔽的是,某些客户端(如旧版 PHP PDO)对 JSON 字段返回字符串而非对象,后续解析易出错。
实操建议:
强制转义所有字符串字段:JSON_OBJECT( 'id', NEW.id, 'name', JSON_QUOTE(NEW.name), 'updated_at', JSON_QUOTE(NEW.updated_at) )若 MySQL 版本 CONCAT('{', ... , '}') 拼接,但必须手动处理单引号、反斜杠、双引号 —— 不推荐,优先升级 日志表的
JSON字段设默认值
NULL,不要设
''或
'{}',否则 JSON_VALID()检查会失败
高并发下触发器写日志导致性能抖动
每条 DML 都触发一次
INSERT,在 QPS 过千的表上,日志表会成为瓶颈:索引更新、磁盘刷写、MVCC 版本链拉长都会拖慢主表。更严重的是,如果日志表和主表在同一个库,锁竞争(尤其是
auto_inc锁)会让事务等待时间飙升。
实操建议:
日志表单独建库(如audit_db),用不同物理磁盘或 SSD 分区,减少 I/O 冲突 日志表去掉非必要索引,只保留
created_at的普通索引(用于按天归档),禁用全文、前缀、函数索引 用
INSERT DELAYED(MySQL 5.6 及以前)已废弃,替代方案是异步化:触发器里只写轻量消息到
sys_log_buffer内存表(
MEMORY引擎),再由定时任务批量落盘 日志字段设计容易被忽略的是时区一致性——
NOW()和
CURRENT_TIMESTAMP在连接级时区设置下可能和系统时钟不一致,建议日志表用
TIMESTAMP类型(自动转 UTC 存储),并在应用层或触发器中显式调用
CONVERT_TZ(NOW(), @@session.time_zone, '+00:00')。
