靠唯一约束+insert ignore或on duplicate key update才能实现幂等;无唯一索引的insert必然不幂等,因无法识别业务重复性,易致数据重复或覆盖错误。

并发写入时 INSERT INTO ... VALUES 报唯一键冲突或主键重复
ThinkPHP 默认用 Db::insert() 或模型 save() 写入请求统计日志,但没做幂等控制。多个请求几乎同时到达,都查不到记录、都走插入逻辑,就会撞上 UNIQUE KEY (date, module, action) 或自增主键重复(如果用了 REPLACE/INSERT IGNORE 但没配好)。错误典型是:SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry。
实操建议:
- 把统计表主键设为业务联合唯一键(如
UNIQUE KEY `uk_date_module_action` (`date`, `module`, `action`)),而非依赖自增 ID - 改用
INSERT ... ON DUPLICATE KEY UPDATE语句,ThinkPHP 里用Db::execute()手写更可控 - 避免先
select再insert/update的“检查-执行”两步法——这中间永远有竞态窗口
用 Db::execute() 执行 INSERT ... ON DUPLICATE KEY UPDATE
ThinkPHP 的 save() 和 insert() 不直接支持 ON DUPLICATE KEY UPDATE,必须手写 SQL。关键点是:字段名要严格匹配,更新值要用 VALUES(xxx) 引用插入值,而不是写死变量。
示例(统计接口调用次数):
$sql = "INSERT INTO `log_request_count` (`date`, `module`, `action`, `count`)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE `count` = `count` + 1";
Db::execute($sql, [$date, $module, $action]);
注意:
-
$date建议用date('Y-m-d'),别用时间戳或带时分秒,否则索引失效 - 表必须已有
UNIQUE KEY(date, module, action),否则ON DUPLICATE不触发 - 不要在 SQL 里拼接变量(如
"'{$date}'"),必须用参数绑定防注入
高并发下 count 字段更新不准怎么办
即使用了 ON DUPLICATE KEY UPDATE,MySQL 默认的 REPEATABLE READ 隔离级别下,多个事务同时执行 count = count + 1 可能读到相同旧值,导致最终只加了 1 次(实际应加 N 次)。这不是 ThinkPHP 的问题,是 MySQL 行锁粒度和事务快照机制导致的。
解决路径只有两个方向:
- 强制串行:给该记录加
SELECT ... FOR UPDATE锁(性能差,不推荐用于统计类场景) - 换原子操作:用
INSERT ... ON DUPLICATE KEY UPDATE count = count + VALUES(count)—— 注意这里是VALUES(count),不是写死数字;上面示例中VALUES(1)是对的,因为每次插入的增量就是 1 - 更彻底的方案:把实时计数换成异步写入(如进 Redis 队列,后台脚本批量落库),但会丢失精确毫秒级数据
Redis 缓存计数 + 定时合并入库是否更稳
如果业务允许分钟级延迟(比如运营看板刷新周期 > 1 分钟),用 Redis 的 INCRBY 做预统计,比直写 MySQL 更扛并发。关键是设计好 key 结构和合并时机。
示例 key:req:count:20240520:api:user:login,过期时间设为 2 天防堆积。
定时任务(如每 5 分钟跑一次)用 SCAN 扫描匹配 key,用 GETDEL 或 DEL + GET 取值并清空,再批量写入 MySQL。注意:
- 别用
KEYS,会阻塞 Redis - 合并时仍要用
INSERT ... ON DUPLICATE KEY UPDATE,因为定时任务也可能并发执行 - 需要记录最后合并时间戳,避免漏掉刚写入还没被扫到的 key
真正难的不是写代码,是确认你的“统计精度”需求到底是什么——是必须每请求都不丢,还是允许误差±0.1%?后者的话,Redis 方案省心很多;前者,就得接受 MySQL 行锁和重试逻辑带来的复杂度。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











