应使用AFTER而非BEFORE触发器记录审计日志,确保数据已落库、主键生成、事务完整;审计表须按字段粒度拆分记录,含operation_type、column_name、old_value、new_value等核心字段,禁用复杂查询与存储过程。

触发器里用 AFTER INSERT/UPDATE/DELETE 而不是 BEFORE
审计日志要记录“已经发生的变更”,不是“准备发生的变更”。BEFORE 触发器中,NEW 和 OLD 值可能未完全稳定(比如自增主键在 BEFORE INSERT 时还是 NULL),且事务尚未提交,日志写入后若事务回滚,审计记录就失真了。AFTER 确保数据已落库、主键已生成、事务上下文完整。
实操建议:
-
AFTER INSERT:只读NEW.*,记录插入行全量字段(或按需选列) -
AFTER UPDATE:对比OLD.col和NEW.col,只记录实际变化的字段,避免日志膨胀 -
AFTER DELETE:只读OLD.*,注意外键级联删除时,OLD仍可用
审计表字段设计必须包含 operation_type、old_value、new_value 和 change_time
只存变更前后整行 JSON 或拼接字符串看似省事,但查起来极慢、无法索引、难做字段级统计。审计不是备份,核心是“谁在什么时候改了哪一列、从什么值变成什么值”。
推荐最小化结构(以用户表为例):
CREATE TABLE user_audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
table_name VARCHAR(64) NOT NULL,
row_id BIGINT NOT NULL, -- 被操作行的主键值
column_name VARCHAR(64) NOT NULL, -- 变更字段名
operation_type ENUM('INSERT','UPDATE','DELETE') NOT NULL,
old_value TEXT, -- JSON 或 VARCHAR,取决于字段类型
new_value TEXT,
change_time DATETIME DEFAULT CURRENT_TIMESTAMP,
changed_by VARCHAR(128) -- 可从 USER() 或应用层传入
);
关键点:
- 不冗余存储整行数据,按字段粒度拆分记录,一条 UPDATE 可能生成多条审计行
-
row_id必须是原表主键值,不能用 UUID 字符串直接存——否则关联查询性能崩盘 -
changed_by别依赖USER(),它返回的是 MySQL 账户(如'app@10.0.1.5'),应由应用在会话中设@current_user_id后读取
触发器体里禁止调用存储过程或复杂函数,尤其不能写 SELECT ... INTO 或子查询
MySQL 触发器运行在语句执行的临界路径上,任何阻塞、锁等待、IO 或计算开销都会直接拖慢业务 SQL。曾见有人在 AFTER UPDATE 里查关联表补用户名,结果主表更新卡住,引发连接池耗尽。
安全做法:
- 所有逻辑用内置函数完成:
JSON_OBJECT()拼字段、IF()判空、CONCAT()组字符串 - 避免
SELECT ... INTO:触发器内不允许读本表(会报Can't update table 't' in stored function/trigger),也别读其他大表 - 不处理 NULL 值转换:用
IFNULL(NEW.name, 'NULL')而非COALESCE(NEW.name, ''),后者在某些版本有隐式类型转换陷阱 - 批量操作慎用:
INSERT INTO ... SELECT触发一次触发器,但影响 N 行;触发器内不能用循环,得靠单条 INSERT 多值解决
上线前必须验证 INSERT ON DUPLICATE KEY UPDATE 和 REPLACE INTO 的行为差异
这两种语法看似等价,但触发器响应完全不同:REPLACE INTO 实质是 DELETE + INSERT,会触发 DELETE 和 INSERT 两个触发器;而 INSERT ... ON DUPLICATE KEY UPDATE 只触发 INSERT 或 UPDATE 中的一个(取决于是否冲突)。审计日志会因此多出无意义的 DELETE 记录,或漏掉真实变更。
应对方式:
- 统一团队 DML 规范,禁用
REPLACE INTO,全部走INSERT ... ON DUPLICATE KEY UPDATE - 若旧系统已用
REPLACE,审计触发器里加判断:IF OLD.id IS NOT NULL THEN /* 是 REPLACE 导致的 DELETE */,再过滤掉这类伪删除 - 测试时务必覆盖主键冲突、唯一索引冲突两种场景,观察触发器实际触发次数
真正麻烦的从来不是写触发器,而是当某天发现审计表比业务表还大十倍、凌晨慢查全是 INSERT INTO audit、或者财务对账时发现某次 REPLACE 操作在日志里留下了一删一插两条记录却没人认领——这些细节不提前掐住,后面就是救火。











