Laravel firstOrNew产生重复数据问题排查与解决咨询
碰到过一模一样的问题!在800-1000+在线用户的高并发场景下,用firstOrNew向JawabanUserM插入数据出现重复,核心问题出在非原子操作和缺乏数据库层面的唯一约束这两点上,结合你的环境很容易触发竞态条件。
问题成因
firstOrNew的非原子特性:这个方法的执行逻辑是先跑SELECT查询记录是否存在,不存在就创建新模型实例,最后调用save()执行INSERT。这两步完全独立,高并发时多个请求会同时通过SELECT判断“记录不存在”,随后各自执行INSERT,最终产生重复数据。- 缺少数据库唯一约束:如果
JawabanUserM表没有针对业务唯一的字段(比如user_id+question_id这类组合)设置唯一索引/约束,MySQL不会主动拦截重复插入请求——哪怕业务逻辑上这些记录本该唯一。 - PHP-FPM的无状态多进程:PHP-FPM是多进程模式,每个请求都是独立进程,没有共享内存或状态,多个进程处理相同用户的答案请求时,完全不知道其他进程的操作,都会走到插入步骤。
解决办法
1. 先加数据库唯一约束(最关键的防线)
无论用什么代码优化,数据库层面的约束都是最后且最可靠的保障。假设业务中一个用户对同一个问题只能有一个答案,给user_id和question_id添加联合唯一索引:
ALTER TABLE JawabanUserM ADD UNIQUE INDEX idx_user_question (user_id, question_id);
这一步必须做,否则任何代码层面的处理都无法100%避免重复。
2. 替换firstOrNew为原子操作方法
方案A:用firstOrCreate并捕获重复异常
firstOrCreate自动完成“查询-不存在则插入”逻辑,当触发唯一约束时会抛出QueryException,我们可以捕获异常并返回已存在的记录:
try { $jawaban = JawabanUserM::firstOrCreate( ['user_id' => $userId, 'question_id' => $questionId], // 唯一查询条件 ['answer' => $answer, 'created_at' => now()] // 插入的额外字段 ); } catch (\Illuminate\Database\QueryException $e) { // 捕获MySQL唯一约束冲突错误(错误码1062) if ($e->getCode() == 1062) { $jawaban = JawabanUserM::where('user_id', $userId)->where('question_id', $questionId)->first(); } else { // 其他异常重新抛出 throw $e; } }
方案B:用updateOrInsert(适合需要更新的场景)
如果用户重复提交答案时需要更新已有记录,updateOrInsert是原子操作,会自动判断插入或更新:
JawabanUserM::updateOrInsert( ['user_id' => $userId, 'question_id' => $questionId], // 唯一匹配条件 ['answer' => $answer, 'updated_at' => now()] // 要插入或更新的字段 );
方案C:用insertOrIgnore(仅确保存在,不更新)
如果只需要确保记录存在、不需要处理更新,insertOrIgnore会忽略唯一约束冲突的错误:
DB::table('JawabanUserM')->insertOrIgnore([ 'user_id' => $userId, 'question_id' => $questionId, 'answer' => $answer, 'created_at' => now(), 'updated_at' => now() ]); // 如需获取已存在的记录,后续再执行一次查询 $jawaban = JawabanUserM::where('user_id', $userId)->where('question_id', $questionId)->first();
3. 事务+悲观锁的补充优化(可选)
如果对数据一致性要求极高,可以在事务中使用悲观锁,但注意这会增加锁等待时间,可能影响性能:
DB::transaction(function () use ($userId, $questionId, $answer) { // 用lockForUpdate()锁定查询结果,防止其他事务插入 $jawaban = JawabanUserM::where('user_id', $userId)->where('question_id', $questionId)->lockForUpdate()->first(); if (!$jawaban) { $jawaban = JawabanUserM::create([ 'user_id' => $userId, 'question_id' => $questionId, 'answer' => $answer ]); } return $jawaban; });
内容的提问来源于stack exchange,提问作者user1927503
相关产品推荐
相关产品推荐

