触发器能自动记录变更,但必须避开事务和性能陷阱:日志与业务共事务,失败则业务回滚;需同库同引擎;禁递归访问、耗时操作;truncate 不触发;按操作类型精准记录字段;user()非真实用户;审计表须合理建索引并定期归档。

触发器能自动记录变更,但必须避开事务和性能陷阱
MySQL 触发器确实能实现审计日志,但不是“加个 trigger 就完事”。它在 INSERT、UPDATE、DELETE 执行时同步触发,意味着日志写入和业务操作共用同一事务——一旦日志表写失败(比如字段类型不匹配、磁盘满),整个业务操作也会回滚。这是最常被忽略的致命点。
- 审计日志表必须和业务表在同一个数据库、使用相同存储引擎(通常是
InnoDB),否则可能报错ERROR 1442: Can't update table in stored function/trigger because it is already used by statement which invoked this stored function/trigger - 避免在触发器里调用存储过程或访问其他业务表,容易引发死锁或递归触发
- 不要在触发器里做耗时操作(如 HTTP 请求、复杂计算),会拖慢主 SQL 执行
- 触发器无法捕获
TRUNCATE TABLE操作——它不走 DML 流程,绕过触发器
INSERT/UPDATE/DELETE 各自该记什么字段
不同操作关注的审计维度不同。硬塞统一字段(比如全记 old_value 和 new_value)既冗余又难查。关键是按需提取,且字段类型要预留足够长度。
-
INSERT触发器只需记录NEW.id、NEW.created_at、操作者(从USER()或应用层传入的audit_user字段取)、action = 'INSERT' -
UPDATE触发器重点对比变化:用IF OLD.name != NEW.name THEN ... END IF判断是否真修改,只记实际变更的字段名和值,避免日志膨胀 -
DELETE触发器只能读OLD.*,建议把关键字段(如OLD.id、OLD.email)转成 JSON 存进before_data文本字段,方便后续还原 - 所有触发器都应记录
NOW()和USER(),但注意USER()返回的是「连接用户」,不是应用层真实操作人;若需真实用户,得让应用在 SQL 中显式传参(如插入时带created_by字段)
审计表设计要支持快速查询,别只图写入简单
一张只含 id、table_name、row_id、action、data 的宽表,半年后查“张三改过哪些订单状态”就抓瞎。索引和结构得提前想清楚。
- 必建复合索引:
INDEX idx_table_action_time (table_name, action, created_at),支撑按表+操作类型+时间范围筛选 - 如果高频按操作人查,给
operator字段单独加索引;但别对before_data/after_data(通常是 TEXT)建全文索引——代价高、效果差,真要搜内容,应用层解析后存到独立关键词字段 -
before_data和after_data推荐用JSON类型(MySQL 5.7+),比TEXT更易校验和抽取;但注意 JSON 字段不能直接ORDER BY,需要配合JSON_EXTRACT() - 定期归档:用
CREATE TABLE audit_log_202406 LIKE audit_log+INSERT INTO ... SELECT ... WHERE created_at 拆分历史数据,别指望单表扛三年
替代方案:为什么有时该放弃触发器
当业务表更新极频繁(如每秒上千次)、或要求审计日志绝对不拖慢主流程时,触发器就是瓶颈。这时候得换思路。
- 应用层双写:业务代码里,在事务提交前,把变更数据发到消息队列(如 Kafka/RabbitMQ),由独立消费者落库。牺牲一点一致性(最终一致),换来主链路零延迟
- Binlog 解析:用
canal、maxwell或Debezium订阅 MySQL binlog,实时捕获所有变更。不侵入业务逻辑,还能捕获TRUNCATE,但部署运维成本高 - 临时方案救急:如果只是排查问题,开启
general_log并定向过滤UPDATE/DELETE语句,比硬上触发器快得多——但别长期开着,IO 压力巨大
触发器适合中小流量、强一致性要求、且 DBA 能控全局的场景。一旦发现 SHOW PROCESSLIST 里大量线程卡在 Waiting for table level lock,基本就是触发器写的日志表成了热点。











