必须用AFTER触发器,因BEFORE中INSERT无OLD、NEW可能被覆盖且事务未提交,易致日志脏数据;AFTER确保数据已落盘、OLD/NEW稳定、上下文可靠。

必须用 AFTER 触发器,且审计表字段设计、JSON 构造、权限和时序控制这四点任一出错,日志就静默失败——不是没写,是写不进去或写错位置。
为什么必须用 AFTER 而不是 BEFORE
BEFORE 触发器里 OLD 在 INSERT 时根本不存在,NEW 的值还可能被后续语句覆盖;更关键的是事务未提交,日志写入后若回滚,audit 表里就留下一堆无效脏数据。AFTER 才能确保:数据已落盘(至少在当前事务内可见)、OLD/NEW 值稳定、操作人上下文可取。
对应关系很明确:
-
AFTER INSERT:只可用NEW.*,OLD报错 -
AFTER UPDATE:OLD.*是变更前值,NEW.*是最终值 -
AFTER DELETE:只可用OLD.*,NEW报错
audit_log 表结构怎么设才不拖慢主表
字段冗余、索引滥用、TEXT 全量存值,是让触发器变成性能瓶颈的三大元凶。别照搬业务表结构往 audit 表里塞。
实操建议:
- 主键用
id BIGINT AUTO_INCREMENT,别用 UUID 或复合主键 -
table_name VARCHAR(64)+record_id VARCHAR(255)组合,覆盖整型/字符串/联合主键场景 -
old_data和new_data必须用JSON类型,不是TEXT;MySQL 5.7+ 的 JSON 函数才能安全处理 NULL 和嵌套 - 时间字段用
created_at TIMESTAMP DEFAULT NOW(3),别用CURRENT_TIMESTAMP——后者在某些版本中会被优化成语句开始时间 - 禁止加除主键外的二级索引;真要查某张表某段时间变更,用
WHERE table_name = ? AND created_at BETWEEN ? AND ?配合主键范围扫描更稳
触发器里拼 JSON 容易掉进哪些坑
JSON_OBJECT('col', NEW.col) 遇到 NEW.col IS NULL 就直接报 Invalid JSON text,整个触发器中断,主 DML 却成功——你完全不知道日志漏了。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
正确做法是显式过滤 + 合并:
- UPDATE 场景下,每个字段都用
IF(OLD.col != NEW.col, JSON_OBJECT('col', NEW.col), NULL) - 用
JSON_MERGE_PATCH('{ }', ...)合并多个字段的 JSON 片段,比CONCAT拼字符串安全得多 - 最终插入前,所有 JSON 字段都套一层
COALESCE(..., '{}'),确保不会传 NULL 进去 - BLOB/TEXT 字段别存全文,改用
LENGTH(col)或MD5(col),否则单次触发延迟可达毫秒级
如何拿到真实操作人而非数据库连接账户
USER() 返回的是 app@10.0.1.5 这种连接账号,不是业务系统里的 user_id=12345。MySQL 没有内置“当前登录用户 ID”变量。
唯一可靠路径是业务层配合:
- 每次执行 DML 前,先跑一句
SET @current_user_id = 12345; - 触发器里统一用
IFNULL(@current_user_id, -1)取值 - DBA 必须给执行用户额外授予
INSERT权限到audit_log表——很多人只给了业务表权限,audit 表 INSERT 失败时不会报错,只会静默跳过 - 避免在触发器里调用含事务控制的存储过程,或做
SELECT ... FOR UPDATE,会触发Can't update table 'xxx' in stored function or trigger
最常被忽略的一点:audit_log 表引擎必须是 InnoDB,且要和业务表在同一个事务中——这样主表回滚时日志也消失,才是符合审计一致性的行为。别为了“日志永存”把它改成 MyISAM,那反而破坏了原子性。










