触发器中判断“最近1分钟发了几条”需用current_timestamp减60秒并配合复合索引,避免依赖now()导致事务内漏判;mysql用signal抛错需declare handler,postgresql可直接raise exception;高并发下建议改用redis限流。

触发器里怎么判断“最近1分钟发了几条”
直接查 INSERT 前的表数据就行,但必须用子查询限制时间范围。常见错误是写成 SELECT COUNT(*) FROM posts WHERE user_id = NEW.user_id AND created_at > NOW() - INTERVAL 1 MINUTE —— 这在 MySQL 中可行,但在 PostgreSQL 里 NOW() 是事务开始时间,可能漏判同一事务内已插入的记录。更稳妥的做法是用 CURRENT_TIMESTAMP(PostgreSQL)或显式传入时间戳字段值。
实际操作建议:
- 确保被查询的表有复合索引:
CREATE INDEX idx_user_created ON posts (user_id, created_at),否则每次 INSERT 都全表扫描 - 把时间窗口设为固定秒数(如 60),别用
INTERVAL 1 MINUTE这类表达式,部分数据库对函数索引支持弱 - 触发器逻辑里先查再决定是否
RAISE EXCEPTION(PostgreSQL)或SIGNAL SQLSTATE '45000'(MySQL 8.0+),不能靠应用层兜底
MySQL 和 PostgreSQL 触发器写法差异在哪
核心区别在语法结构和错误抛出方式。MySQL 要求触发器体用 BEGIN ... END 包裹,且 SIGNAL 必须配合 DECLARE CONTINUE HANDLER 才能中断;PostgreSQL 可直接在 IF 分支里 RAISE EXCEPTION。
示例(PostgreSQL):
CREATE OR REPLACE FUNCTION check_post_rate()
RETURNS TRIGGER AS $$
BEGIN
IF (SELECT COUNT(*) FROM posts
WHERE user_id = NEW.user_id
AND created_at > CURRENT_TIMESTAMP - INTERVAL '60 seconds') >= 5 THEN
RAISE EXCEPTION 'exceeds post limit: 5 per minute';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
MySQL 注意点:
-
NEW.created_at必须明确赋值(比如默认NOW()),否则子查询查不到刚插入的这条 - 不能在触发器里调用存储函数返回动态限值,MySQL 对函数内查询限制多,容易报错
Can't update table 'posts' in stored function/trigger - MySQL 5.7 不支持
SIGNAL,得用INSERT INTO non_existent_table强制报错,不推荐
为什么生产环境慎用触发器做频率控制
不是不能用,而是并发高时容易成为瓶颈。每个 INSERT 都要执行一次子查询 + 索引查找,QPS 上千后 DB CPU 明显升高;更麻烦的是,它无法跨实例限流——比如用户从 App 和 Web 同时发帖,触发器只管单库,漏掉一半请求。
替代方案更实用:
- 用 Redis 的
INCR+EXPIRE做分布式计数,原子性好、延迟低 - 把频率检查提到 API 网关层(如 Kong、Nginx + Lua),避免请求到底层 DB
- 如果坚持用 DB,至少把窗口粒度放大(如 5 分钟 20 条),减少索引扫描压力
触发器没生效?先查这三件事
最常踩的坑不是逻辑错,而是配置级问题:
- MySQL 检查
sql_mode是否含STRICT_TRANS_TABLES,否则SIGNAL可能静默失败 - PostgreSQL 触发器是否绑定到正确事件:
FOR EACH ROW BEFORE INSERT,写成AFTER就晚了 - 触发器函数里用了
SELECT但没加INTO或RETURN,PostgreSQL 会报query has no destination
真正难的是边界情况:用户删帖后是否重置计数?触发器默认不管 DELETE,得额外写 DELETE 触发器,或者干脆不删只软删——这事得跟业务方对齐,别自己拍板。











