触发器不能直接发邮件或调用外部服务,只能写入告警队列表(如alarm_queue),需处理null、类型转换、去重、频率控制及状态机消费。

触发器里不能直接发邮件或调用外部服务
SQL 触发器(比如 BEFORE INSERT 或 AFTER UPDATE)本质是事务内执行的数据库逻辑,不支持阻塞式网络调用。你写 SEND MAIL、curl、EXEC xp_cmdshell 这类操作,要么语法报错,要么被禁用(尤其在生产环境默认关闭),要么导致事务卡死或回滚失败。
常见错误现象:ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA(MySQL)、PostgreSQL 报 cannot execute INSERT in a read-only transaction(若试图写日志表但权限不足)、SQL Server 报 Ad hoc distributed queries are not enabled。
- 正确做法:只做「标记」和「落库」——把告警条件满足的记录写入一张专用告警队列表,比如
alarm_queue,字段含event_type、threshold_value、trigger_time、record_id - 必须避开事务回滚风险:用
INSERT ... SELECT或INSERT ... VALUES显式插入,别依赖触发器外的临时表或变量 - 注意隔离级别影响:高并发下,多个触发器可能同时写同一条告警记录,建议在
alarm_queue上加唯一约束(如(record_id, event_type, DATE(trigger_time)))或用应用层去重
阈值判断要防浮点精度与 NULL 陷阱
触发器中做数值比较(比如 NEW.temperature > 40.0)看着简单,实际容易因数据类型隐式转换或 NULL 传播失效。MySQL 默认把 DECIMAL 字段参与计算时转成 double,PostgreSQL 对 NULL 比较直接返回 UNKNOWN,整个 IF 条件就跳过。
典型翻车场景:温度字段是 DECIMAL(5,2),但触发器里写 IF NEW.temp > 40 THEN ...,结果 40.00 和 40 类型不一致;或者某条记录 NEW.temp 是 NULL,NULL > 40 不成立也不报错,直接静默跳过。
- 显式类型转换:MySQL 用
CAST(NEW.temp AS DECIMAL(5,2)) > 40.00;PostgreSQL 用NEW.temp::numeric > 40.00 - NULL 必须单独处理:写成
IF NEW.temp IS NOT NULL AND NEW.temp > 40.00 THEN ...,别指望COALESCE(NEW.temp, 0) > 40—— 阈值逻辑变了 - 避免用
BETWEEN做阈值区间:它包含边界,而报警常需「严格超限」,推荐用/ <code>>明确方向
告警去重与频率控制得靠时间窗口+状态字段
如果每条超温记录都触发一次告警写入,1 秒内插入 100 条数据,就会塞满 alarm_queue 表,下游消费端压力大,还可能重复通知。纯靠触发器没法 sleep 或计数,必须借力数据库已有机制。
常见错误:在触发器里查 SELECT COUNT(*) FROM alarm_queue WHERE record_id = NEW.id AND trigger_time > NOW() - INTERVAL 1 HOUR —— 这会引发表锁或死锁,尤其在高并发更新同一主表时。
- 轻量方案:给告警队列表加复合唯一索引,比如
UNIQUE (record_id, DATE(trigger_time)),冲突时忽略(MySQL 用INSERT IGNORE,PostgreSQL 用ON CONFLICT DO NOTHING) - 更准方案:在主表加一个
last_alarm_time时间戳字段,触发器里先SELECT last_alarm_time FROM main_table WHERE id = NEW.id(需确保该查询走索引),再判断是否超间隔,最后UPDATE main_table SET last_alarm_time = NOW() WHERE id = NEW.id - 注意:第二个方案要求触发器是
AFTER UPDATE且主表有主键/索引支撑,否则性能崩得比告警还快
下游消费程序才是告警真正的“出口”
触发器只管把条件满足的事件记下来,真正发邮件、推钉钉、写 Kafka 的,必须交给外部程序轮询或监听 alarm_queue。这步漏掉,前面全白干。
容易被忽略的细节:alarm_queue 表没加索引,消费程序 SELECT * FROM alarm_queue WHERE status = 'pending' LIMIT 100 全表扫;或者没设 status 字段,消费后直接 DELETE,一旦程序崩溃,告警就丢了。
- 必加索引:
CREATE INDEX idx_alarm_pending ON alarm_queue (status, trigger_time) - 状态机设计:字段用
status ENUM('pending', 'processing', 'sent', 'failed'),消费时先UPDATE ... SET status = 'processing' WHERE id = ? AND status = 'pending'(CAS 更新) - 失败重试:
failed状态记录失败原因到error_message字段,避免无限重试压垮下游
复杂点不在触发器本身,而在怎么让队列表不积压、不丢事件、不重复投递——这些事,SQL 触发器一句也干不了。










