核心思路是:在INSERT触发器中用COUNT(*)查当前用户当日记录数,WHERE条件必须用created_at >= DATE(NOW())确保自然日统计,超阈值则调用自定义错误过程;但该方案非原子操作,并发下易失效,应配合Redis预检或计数表+SELECT FOR UPDATE锁行。

触发器里怎么统计当天写入次数
核心思路是:在 INSERT 触发器中,用 COUNT(*) 查当前用户今天已有的记录数,再和阈值比较。注意必须用 WHERE user_id = NEW.user_id + 时间范围(如 created_at >= CURDATE()),不能只查全表——否则并发插入时可能漏判。
常见错误是用 NOW() - INTERVAL 1 DAY,这会查过去24小时而非“今天0点起”,导致跨日边界时统计不准。正确写法是:
WHERE created_at >= DATE(NOW()) AND created_at < DATE_ADD(DATE(NOW()), INTERVAL 1 DAY)
如果字段类型是 DATETIME 或 TIMESTAMP,DATE(created_at) = DATE(NOW()) 更直观,但注意它无法走索引;加函数的字段要建函数索引(MySQL 8.0+)或改用范围查询。
触发器中抛出错误阻止插入
MySQL 不支持在触发器里直接 RETURN 或 EXIT,必须用异常中断流程。最稳妥的方式是调用一个不存在的存储过程,触发 ERROR 1305 (42000): PROCEDURE xxx does not exist:
IF @count_today >= 5 THEN CALL `trigger_error_too_many_inserts`; END IF;
这个技巧稳定、无副作用,比用 SIGNAL(需 MySQL 5.5+ 且权限足够)更兼容旧版本。注意:不能用 SELECT 抛错(如 SELECT 1/0),某些客户端会忽略该错误继续执行。
使用场景:防刷接口后端写库前未校验,靠数据库兜底;或业务逻辑分散,统一由 DB 层控频。
并发写入下计数不准怎么办
触发器内查 + 判 + 拒绝不是原子操作,高并发时两个事务几乎同时查到“还剩1次”,都通过校验,结果超限。根本解法只有加锁或序列化:
- 用
SELECT ... FOR UPDATE锁住该用户的计数行(需提前建一张user_daily_counter表) - 或改用应用层 Redis 计数(
INCR+EXPIRE),DB 触发器只做最终校验 - 若坚持纯 DB 方案,可将判断逻辑移到存储过程中,用
GET_LOCK()做轻量级互斥(但锁名需含user_id和日期,避免全局争抢)
单纯依赖触发器查表计数,在 QPS > 50 的场景大概率失效。别被“语法能跑通”骗了。
为什么不用 CHECK 约束或应用层限频就够了
CHECK 约束只能校验单行数据,无法访问其他行或聚合结果,完全不适用。应用层限频看似更合理,但存在盲区:
— 多实例部署时,本地内存计数不同步
— 管理后台直连 DB 插入绕过业务代码
— 第三方 ETL 工具批量导入
触发器是最后一道防线,但它不该是第一道。真实方案通常是:Redis 预检(快) + 触发器兜底(稳) + 定期对账脚本(准)。单独押注触发器,等于把防刷逻辑绑死在数据库上,扩展性和可观测性都很差。
真正容易被忽略的是时间精度——如果业务要求“自然日”,而数据库服务器时区和应用不一致,DATE(NOW()) 返回的就不是你认为的“今天”。务必确认 SELECT @@time_zone 和应用所在时区一致。

















