审计触发器应仅部署在orders、users、payments等核心敏感表上,避开日志表以防递归;需按操作类型区分使用old/new变量;时间与用户信息须由应用层传入,避免函数失真;日志表宜用固定字段+分区详情表设计。

触发器写在哪个表上才真正有用
审计不是给所有表都加触发器,而是只在业务核心的orders、users、payments这类被频繁修改且敏感的表上部署。在日志表或配置表上加AFTER INSERT触发器,除了拖慢写入,几乎不产生有效审计信息。
常见错误是把触发器建在audit_log自身上——结果每次写日志又触发一次日志,无限递归,MySQL 直接报错 ERROR 1442: Can't update table 'audit_log' in stored function/trigger。
- 必须避开日志表自身,用
INSERT INTO audit_log ... SELECT方式写入,不触发新事件 - 如果业务有分库分表,触发器只能作用于单表,跨分片操作无法捕获,这时得换方案(如 Binlog 解析)
- PostgreSQL 支持
CREATE OR REPLACE FUNCTION配合pg_notify异步投递,但 MySQL 没这能力,同步写是唯一选择,务必压测写入吞吐
OLD 和 NEW 在 DELETE/UPDATE 中怎么安全取值
OLD在INSERT触发器里根本不存在,NEW在DELETE里也为空——这不是 bug,是 MySQL 的语义设计。硬写OLD.id进INSERT触发器,会直接报错 ERROR 1327: Undeclared variable: OLD。
实际审计最常踩的坑,是想统一用IF OLD.id IS NOT NULL THEN ...来兼容三种操作,但触发器类型(BEFORE/AFTER INSERT/UPDATE/DELETE)决定了可用变量,不能混用。
-
INSERT:只可用NEW;DELETE:只可用OLD;UPDATE:两者都可用 - 审计字段建议统一用
CONCAT('id=', COALESCE(NEW.id, OLD.id)),避免NULL拼接导致整条日志变空 - 别在
BEFORE触发器里改NEW字段来“记录变更前值”——那是逻辑污染,审计要的是原始操作快照,不是中间态
触发器里调用 NOW() 和 USER() 为什么不准
NOW()返回的是触发器执行时刻的时间,不是原始 SQL 发起时间;USER()返回的是执行触发器的数据库账号(比如app@10.0.1.%),不是发起请求的应用用户(比如alice@webapp)。这两者在连接池复用、代理层转发场景下完全失真。
更麻烦的是,MySQL 5.7+ 对触发器内函数调用有隐式限制:CURRENT_USER()能拿到授权用户,但若应用用统一账号连库,它和USER()一样毫无区分度。
- 真实用户需由应用层传入,例如在
INSERT语句中显式带created_by VARCHAR(64)字段,触发器从NEW.created_by取值 - 时间戳优先用应用生成的
created_at字段,而非触发器里算;若必须用服务端时间,至少用SYSDATE()(更接近执行瞬间,但仍有微秒级偏差) - 禁止在触发器里调用存储过程查用户映射表——会锁表、拖慢主业务,审计不能成为性能瓶颈
审计日志表结构怎么防膨胀又保可查
审计表不是越全越好。TEXT字段存完整OLD/NEW JSON 快速撑爆磁盘,而只存changed_fields字符串又查不出上下文。折中方案是固定字段 + 可选大字段分离。
另一个隐形问题是字符集:如果业务表用utf8mb4,但审计表建成了latin1,中文字段一写就乱码,日志等于白记。
- 必存字段:
table_name、operation('INSERT'/'UPDATE'/'DELETE')、row_id(主键值)、changed_at(应用传入)、operator(应用传入) - 可选大字段单独建
audit_log_detail表,用log_id关联,按月分区,冷数据定期归档到对象存储 -
operation字段别用TINYINT编码(比如 1=INSERT),运维查日志时得翻文档,直接用ENUM('INSERT','UPDATE','DELETE')或VARCHAR(10)更直觉
触发器本身不解决权限隔离问题。DBA 能删日志,应用也能绕过触发器直连写表——轻量审计的前提,是信任基础设施和最小权限配置。这点容易被忽略,但比语法细节重要得多。










