MySQL触发器中禁止更新触发它的原表,必须使用独立汇总表;需用ON DUPLICATE KEY UPDATE处理并发冲突;NEW.created_at为空时应设默认异常月份;高并发下应弃用触发器改用异步批处理。

触发器里不能用 SELECT ... FOR UPDATE 或事务嵌套
MySQL 的 AFTER INSERT 触发器里,对同一张表做 UPDATE 汇总是安全的,但若汇总目标表恰好是当前正在插入的表(比如在 orders_202401 上建触发器去更新 orders_202401 自身的统计行),会直接报错 Can't update table 'orders_202401' in stored function/trigger because it is already used by statement which invoked this stored function/trigger。这是 MySQL 的限制,不是 bug。
实操建议:
- 汇总目标必须是**独立的汇总表**,例如
orders_summary,字段为month,total_amount,order_count - 触发器中避免任何对原表(即
INSERT目标表)的读写操作,只操作汇总表 - 如果分表名带时间(如
orders_202401),可在触发器里用YEAR(NEW.created_at)和MONTH(NEW.created_at)动态计算归属月份,而不是解析表名字符串
INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 是必须的
触发器执行期间,可能多个并发插入命中同一个月(比如 2024-01-01 和 2024-01-31 都落在 202401),导致重复插入汇总行而报 Duplicate entry '202401' for key 'PRIMARY'。靠 IF NOT EXISTS 在触发器里查再插,既慢又不安全(竞态依然存在)。
实操建议:
- 汇总表主键设为
month CHAR(6) PRIMARY KEY(如'202401') - 触发器内统一用
INSERT INTO orders_summary (month, total_amount, order_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE total_amount = total_amount + VALUES(total_amount), order_count = order_count + VALUES(order_count) - 不要用
REPLACE INTO,它本质是 DELETE + INSERT,会引发自增 ID 跳变和外键级联风险
NEW.created_at 为空时会汇总到错误月份
如果业务允许 created_at 为 NULL,而触发器里直接写 YEAR(NEW.created_at),MySQL 会返回 0000,拼出 '000000' 这种非法月份,后续查询或分区都可能出问题。
实操建议:
- 在触发器开头加判断:
IF NEW.created_at IS NULL THEN SET @month_key = '999999'; ELSE SET @month_key = DATE_FORMAT(NEW.created_at, '%Y%m'); END IF; - 把
'999999'当作“异常数据汇总桶”,单独监控,避免污染正常月度统计 - 更稳妥的做法是在应用层强制
created_at非空,数据库字段设为NOT NULL DEFAULT CURRENT_TIMESTAMP
触发器性能瓶颈在高并发 INSERT 场景下立刻暴露
每条订单插入都触发一次汇总更新,当单表 QPS 超过 500,orders_summary 表会成为热点,出现大量锁等待,SHOW PROCESSLIST 可见一堆 Updating 状态。
实操建议:
- 给汇总表的
month字段加UNIQUE索引(主键已满足) - 避免在触发器里调用函数如
CONCAT(YEAR(...), LPAD(MONTH(...),2,'0')),全部用DATE_FORMAT(NEW.created_at, '%Y%m')一步到位 - QPS 持续 > 1000 时,应放弃触发器方案,改用应用层异步批处理(如 Kafka + Flink)或定时任务(每分钟聚合一次原始分表)
触发器适合低频、强实时性要求且数据量可控的场景;一旦涉及秒级万级写入,它就不是“实时累加”的解法,而是故障点本身。

















