必须用BEFORE UPDATE记录敏感字段变更,因其在数据落盘前捕获稳定OLD/NEW值,支持回滚一致性;AFTER中值可能被覆盖且事务回滚会导致脏日志。

为什么必须用 BEFORE UPDATE 而不是 AFTER
敏感字段(如 phone、email、id_card)的变更审计,唯一可靠时机是 BEFORE UPDATE。AFTER 中 OLD/NEW 值虽语法可用,但高并发下可能被同一事务内后续语句覆盖;更关键的是,若业务逻辑在 UPDATE 后抛错并回滚,AFTER 触发器已写入的日志就变成脏数据——你看到“改了”,实际没生效。
BEFORE 则不同:它在数据落盘前捕获原始状态,值稳定、可回滚、上下文干净。INSERT 和 DELETE 场景不适用此逻辑,因为它们没有“变更中”的中间态。
如何只记录真正变化的敏感字段
别写一堆 IF OLD.phone != NEW.phone THEN ... END IF,维护成本高还容易漏字段。推荐用 UNION ALL 子查询一次性提取所有变动:
INSERT INTO audit_log (table_name, record_id, field_name, old_value, new_value, updated_at) SELECT 'users', NEW.id, field_name, old_val, new_val, NOW() FROM ( SELECT 'phone' AS field_name, OLD.phone AS old_val, NEW.phone AS new_val WHERE OLD.phone IS NOT DISTINCT FROM NEW.phone = FALSE UNION ALL SELECT 'email', OLD.email, NEW.email WHERE OLD.email IS NOT DISTINCT FROM NEW.email = FALSE ) AS changes;
要点:
-
IS NOT DISTINCT FROM正确处理NULL比较,!=在任一操作数为NULL时直接返回NULL,导致条件失效 - 每个字段判断独立,互不影响;新增字段只需加一段
UNION ALL - 避免在触发器里调用函数或子查询以外的逻辑——MySQL 触发器不支持事务嵌套,出错会中断主 DML
audit_log 表结构怎么防性能崩盘
敏感字段审计最常翻车的不是逻辑,而是表设计。以下三点必须卡死:
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
- 主键用
audit_id BIGINT AUTO_INCREMENT,禁用UUID或复合主键——插入吞吐直接掉 30%+ -
old_value和new_value必须是VARCHAR(255)或TEXT,**不能存 BLOB/TEXT 字段全文**;改用MD5(col)或LENGTH(col),否则单次触发延迟从微秒级跳到毫秒级 - 索引只建两个:
(table_name, record_id)用于按记录查历史,(field_name, updated_at)用于查某字段全量变更趋势;别给 JSON 或 TEXT 字段加索引,MySQL 压根用不上
触发器里怎么拿到真实业务用户 ID
USER() 返回的是数据库连接账号(如 'app@10.0.1.5'),不是业务系统的操作人。想记录 user_id = 12345,必须靠应用层配合:
执行业务 SQL 前,先跑一句:SET @current_user_id = 12345;
触发器里取值写成:IFNULL(@current_user_id, -1)
注意:
- 会话变量
@current_user_id是连接级的,断连即丢,不能跨请求复用 - 禁止在触发器里用
CURRENT_USER()替代——它只返回授权用户名(如'app'@'%'),连 IP 都没了 - 如果应用无法改,只能退而求其次存
SUBSTRING_INDEX(USER(), '@', 1),但这就和业务账号完全脱钩了
真正难的不是写触发器,是让每一笔 UPDATE 都带着上下文进来;漏掉一次 SET @current_user_id,那条日志里的操作人就永远对不上。

















