审计触发器写入失败会导致业务事务回滚,因触发器与业务操作共用同一事务;必须将审计表与业务表分离、字段设默认值、禁用高危操作,并采用before触发+异步落库策略。

审计触发器写入失败会导致业务事务回滚
这是最常被忽略的致命点:触发器运行在原事务上下文中,INSERT INTO audit_log 一旦因磁盘满、字段类型不匹配、锁超时或主键冲突失败,整个业务 INSERT/UPDATE 就会一起回滚。用户下单失败,只因为审计表某列少了个 NOT NULL 约束。
- 审计表必须与业务表分离——不同表空间、不同文件组,避免 I/O 争用
- 审计表字段全部设为
NOT NULL DEFAULT ''或DEFAULT CURRENT_TIMESTAMP,禁用任何可能触发约束检查的非空校验 - 所有字符串字段用
VARCHAR(1024)或更大,避免DATA_TOO_LONG错误;JSON 字段统一用TEXT或JSON类型(MySQL 5.7+) - 别在触发器里调用自定义函数做字段转换,比如
get_client_ip()—— 函数内若查information_schema.PROCESSLIST会引发元数据锁,高并发下直接卡死
用 BEFORE INSERT + 异步化兜底代替 AFTER INSERT
标准 AFTER INSERT 审计是同步阻塞的,而 BEFORE INSERT 可以把审计逻辑“压平”到当前事务中更可控的位置,再配合轻量级异步策略降低风险。
- 在
BEFORE INSERT中只做三件事:生成唯一audit_id(用UUID()或CONCAT('a_', UNIX_TIMESTAMP(), '_', FLOOR(RAND()*1000))),填充actor(CURRENT_USER())、action、entity、entity_id,其余字段留空或设默认值 - 把完整变更详情(如
OLD/NEW的 JSON 差异)暂存在内存变量或临时表(MySQL 不支持,可用SET @audit_payload := JSON_OBJECT(...)),不立即写盘 - 真正落库动作交给应用层定时任务或数据库外的消息队列消费——例如每 5 秒批量拉取未完成的
audit_id,补全 payload 后批量插入,失败可重试
禁止在触发器里查 PROCESSLIST 或 performance_schema
很多团队想记录客户端 IP 或应用名,直接在触发器里写 SELECT HOST FROM information_schema.PROCESSLIST WHERE ID = CONNECTION_ID(),这在 MySQL 8.0 下会持有 metadata_lock,QPS 超过 200 就开始排队等待,SHOW PROCESSLIST 里大量线程卡在 Waiting for table metadata lock。
- 客户端信息必须由应用层透传,通过注释方式带入 SQL:
/* app=order-service,ip=10.0.1.23 */ INSERT INTO orders ... - 触发器内用
sys.schema_table_statistics(MySQL 8.0+)或自定义函数解析注释不可行——没有稳定接口,且解析本身开销大 - 真要提取 IP,可在连接池层设置
init_connect='SET @client_ip = SUBSTRING_INDEX(USER(), "@", -1)',然后触发器里直接用@client_ip
审计表主键和索引设计直接影响并发吞吐
审计表变成热点后,INSERT 冲突集中在自增主键或时间字段上,容易引发页分裂和间隙锁竞争。
- 主键别用
id BIGINT AUTO_INCREMENT,改用audit_id CHAR(36) PRIMARY KEY(UUID v4),消除自增锁瓶颈 - 删掉所有非必要索引;只保留一个联合索引:
INDEX idx_entity_time (entity, created_at),覆盖最常用查询场景 -
created_at字段用CURRENT_TIMESTAMP(3)(毫秒精度),避免多行同秒导致索引排序压力 - 定期归档:按月分区(MySQL 8.0+ 支持
PARTITION BY RANGE COLUMNS(created_at)),冷数据DISCARD TABLESPACE脱机处理
实际部署时,最容易被跳过的不是语法细节,而是对“审计失败是否可接受”的业务对齐——如果合同要求每条审计必须 100% 实时落库,那触发器方案本身就不成立,得切到应用层事件总线 + 幂等写入。











