最硬核的重复统计规避方式是数据库联合唯一索引,如uk_ip_uri_date;Redis SETNX作前置过滤需urlencode URI并合理设过期;ThinkPHP中禁用count缓存或直接走唯一索引+异常捕获;聚合统计优先用INSERT...ON DUPLICATE KEY UPDATE。

用数据库唯一索引拦截重复统计写入
最硬核、最省心的规避方式,就是不让重复记录进库。请求统计类场景(比如 request_logs 表记录 IP + URI + 时间戳)一旦出现重复,99% 是因为业务层没做幂等,而不是真有两条独立请求。直接在数据库加联合唯一约束,比任何 PHP 逻辑都可靠。
常见错误是只对 ip 或只对 uri 建唯一索引——这会误拦合法请求。正确做法是组合高频区分字段,例如:
ALTER TABLE request_logs ADD UNIQUE INDEX uk_ip_uri_date (ip, uri, DATE(created_at));
这样同 IP 同接口在同一天只允许一条统计记录。如果需要更细粒度(如每小时),把 DATE() 换成 DATE_FORMAT(created_at, '%Y%m%d%H') 即可。
PHP 层捕获异常时,必须精准识别 MySQL 的重复键错误码:
立即学习“PHP免费学习笔记(深入)”;
-
1062(Integrity constraint violation)才忽略或记录告警 - 其他错误(如连接超时、字段超长)不能一并吞掉
用 Redis SETNX 做轻量级请求指纹过滤
当统计频率极高(比如秒级聚合)、又不能接受数据库报错时,Redis 是第一道防线。关键不是“存什么”,而是“怎么存不冲突”。
推荐 key 设计:stat:ip:{ip}:uri:{uri}:date:{Y-m-d},用 SETNX 写入 + EXPIRE 设置过期(别用 SETEX,老 Redis 版本不支持):
$key = 'stat:ip:' . $ip . ':uri:' . urlencode($uri) . ':date:' . date('Y-m-d');
if ($redis->setnx($key, 1)) {
$redis->expire($key, 86400); // 保证当天内只计一次
// 执行 DB 插入或自增
}
注意点:
- URI 必须
urlencode(),否则带查询参数时 key 会不一致 - 过期时间必须 ≥ 统计窗口周期,否则可能刚过期就又被写入
- 不要依赖 Redis 返回值判断业务成功——它只是过滤器,DB 写入失败仍需单独处理
ThinkPHP 中避免 count() 查询被缓存干扰
有些同学用 Db::name('request_logs')->where(...)->count() 先查重再插入,结果在高并发下依然出重复——根本原因是 ThinkPHP 默认开启查询缓存,且 count() 可能命中旧缓存。
必须显式关闭缓存:
$exists = Db::name('request_logs')
->cache(false) // 关键:禁用查询缓存
->where(['ip' => $ip, 'uri' => $uri, 'date' => date('Y-m-d')])
->count();
if (!$exists) {
Db::name('request_logs')->insert([...]);
}
更稳妥的做法是跳过“先查后插”流程,直接走唯一索引 + 异常捕获,省去一次 DB 查询开销,也避开竞态条件。
聚合统计时用 INSERT ... ON DUPLICATE KEY UPDATE 替代多次 insert
如果统计目标是汇总表(比如按天统计每个接口调用量),频繁 insert 会导致大量重复主键冲突。MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 是更优雅的方案。
ThinkPHP 不原生支持该语法,需手写原生 SQL:
$sql = "INSERT INTO api_stats (date, uri, count) VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE count = count + 1";
Db::execute($sql, [date('Y-m-d'), $uri]);
前提是 api_stats 表已建好联合主键或唯一索引:PRIMARY KEY (date, uri)。
这个方案的优势在于:原子性、无竞态、无异常抛出、性能接近单条 insert。
真正容易被忽略的是时间维度粒度与业务语义的对齐——比如“按小时统计”却用 DATE(created_at) 截断,会导致同一小时内所有请求被合到一条记录里。字段设计和索引定义,得从统计目的反推,而不是照搬日志表结构。



















