触发器不能直接发http请求,必须解耦为“触发器写日志表+外部程序轮询发送”;需用before update判断真实状态变化,避免无效日志,并通过for update skip locked和幂等设计保障消费可靠性。

触发器里不能直接发HTTP请求,得换思路
SQL触发器本身不支持调用外部服务(比如发Webhook、写消息队列),AFTER UPDATE 里直接写 curl 或 http_post() 会报错或被数据库禁止。常见错误现象是触发器编译失败、执行时报 function does not exist,或者干脆静默失败。
真正可行的做法是:在触发器里只做一件事——往一张专用日志表插入记录,把「谁改了哪张单据、改前改后状态、时间、操作人」存下来。后续由外部程序(如Python脚本、定时任务、CDC工具)轮询这张表,再异步发通知。
- 避免在触发器中做耗时操作(如网络IO、复杂计算),否则拖慢主业务更新
- 日志表字段建议至少包含:
id、doc_type(单据类型)、doc_id、old_status、new_status、operator_id、created_at - 给
created_at加索引,方便外部程序按时间范围高效拉取未处理记录
PostgreSQL示例:用BEFORE UPDATE捕获状态变更
用 BEFORE UPDATE 而不是 AFTER,能直接访问 OLD 和 NEW 行,判断是否真发生了状态变化。很多同学漏掉这层判断,导致每次UPDATE都写日志,哪怕状态根本没变。
CREATE OR REPLACE FUNCTION log_status_change()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status != NEW.status THEN
INSERT INTO status_change_log (
doc_type, doc_id, old_status, new_status, operator_id, created_at
) VALUES (
'order', NEW.id, OLD.status, NEW.status, NEW.updated_by, NOW()
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
<p>CREATE TRIGGER trig_order_status_change
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION log_status_change();</p>
注意:NEW.updated_by 需要业务层确保已写入(比如应用代码显式赋值),数据库自己不会自动填操作人。
MySQL 8.0+ 的等效写法和坑点
MySQL 不支持函数返回 NEW,必须用 AFTER UPDATE,且需手动查旧值——这就要求你先在事务里 SELECT 出旧状态,再 UPDATE,否则无法保证一致性。更稳妥的做法是:让应用层在 UPDATE 前先查一次,把旧状态传进来;或者用 INSERT ... ON DUPLICATE KEY UPDATE + 临时表绕过。
- MySQL 触发器不能读写触发表本身(会报
Can't update table 'orders' in stored function/trigger),所以不能在触发器里再查orders - 如果用
AFTER UPDATE,必须依赖SELECT ... FOR UPDATE在应用层加锁,否则并发更新可能漏日志 - 推荐在应用层统一处理状态变更逻辑,把日志写入和业务更新放在同一个事务里,比纯触发器更可控
日志表怎么被安全消费?别卡在“最后一条”
外部程序消费 status_change_log 表时,最容易踩的坑是:用 SELECT * FROM status_change_log WHERE processed = false ORDER BY id LIMIT 100 然后逐条更新 processed = true。一旦某条通知失败,整个批次卡住,后续新日志全堵着。
正确做法是:用 SELECT ... FOR UPDATE SKIP LOCKED(PostgreSQL/MySQL 8.0+ 支持),每次只锁住要处理的几条,失败就跳过并记错,不影响其他行。
更关键的是——别依赖“标记已处理”,而是用幂等通知:每次通知带唯一 event_id(比如 log_id + timestamp),下游收到重复就丢弃。这样即使重试多次也不出错。
真正的难点不在触发器怎么写,而在于日志消费链路的可靠性设计:从轮询间隔、失败重试策略、死信处理,到下游服务是否真收到了。










