Laravel中save()方法数据重复插入问题的排查与解决咨询
推荐记录重复插入问题排查与解决
问题描述
负责实现数据库插入/保存新推荐记录功能,本周在数千条记录中出现3条重复记录。当前数据库未设置唯一约束,想了解现有系统内是否有办法避免该问题,以及可能的根因。已测试双击、慢速网络下双击、断网提交后联网再提交等场景,数据库均仅存储1条记录。
控制器代码
$referral = new Referral; $referral->fullname = $request->fullname; $referral->mobile_phone = $request->phone; $referral->id_registration = $id_reg; //$referral etc... $referral->save(); try { Mail::send('mail.user.referral_creation', compact('email', 'fullname', 'user'), function ($message) use ($template, $email, $fullname, $main_sender_email, $main_sender_name) { $message->from($main_sender_email, $main_sender_name); $message->subject($template->greeting); $message->to($email, $fullname); }); return redirect('/')->with(['success' => 'success']); } catch (\Swift_TransportException $e) { \Log::error($e); return redirect('/')->with(['error' => 'error']); }
Referral模型代码
class Referral extends Model implements Auditable{ use \OwenIt\Auditing\Auditable; protected $table = 'referrals'; protected $fillable = ['fullname', 'mobile_phone','email',...] ]; use SoftDeletes; protected $guarded = ["id"]; protected $dates = ['deleted_at']; public $timestamps = true; }
重复记录时间戳
- 记录1:2023-08-10 17:22:01 / 2023-08-10 17:22:02(相差1秒)
- 记录2:2023-08-14 7:30:36 / 2023-08-14 7:30:36(同一秒)
- 记录3:2023-08-14 17:26:09 / 2023-08-14 17:26:10(相差1秒)
可能的根因分析
未捕获的后端异常导致前端重试
当前代码仅捕获了Swift_TransportException,如果Mail::send抛出其他异常(比如视图渲染错误、变量未定义),会直接返回500错误。部分前端或浏览器会自动重试失败的请求,此时两次请求都会执行save逻辑,生成重复记录。网络/中间件自动重试
负载均衡器、CDN或浏览器的网络重试机制,在检测到请求超时或未及时响应时,会自动重发请求。短时间内的重复请求会绕过前端防重逻辑,直接触发后端的插入操作。前端特殊场景漏测
部分浏览器插件、自动填充工具或移动设备的特殊交互,可能导致表单被重复提交,而常规测试场景未覆盖到这类情况。
现有系统避免重复的方案
1. 前端层面
- 提交按钮点击后立即禁用并添加loading状态,直到请求完成(成功/失败),阻止用户重复点击。
- 生成唯一表单token:页面加载时生成随机token存入session,提交时携带该token,后端校验后立即失效,避免重复提交。
2. 后端层面
(1)插入前校验重复记录
在执行save前,通过关键字段组合(比如mobile_phone+id_registration,可根据业务添加其他字段)查询是否已存在相同记录,不存在再执行插入:
// 在创建Referral实例前添加校验 $isDuplicate = Referral::where('mobile_phone', $request->phone) ->where('id_registration', $id_reg) // 可选:添加fullname等其他标识字段 ->exists(); if (!$isDuplicate) { $referral = new Referral; $referral->fullname = $request->fullname; $referral->mobile_phone = $request->phone; $referral->id_registration = $id_reg; // 其他字段赋值 $referral->save(); }
注:高并发场景下可能存在竞态问题,但当前仅出现3条重复,该方案可覆盖绝大多数场景。
(2)扩大异常捕获范围
将整个业务逻辑纳入异常捕获,避免未捕获异常导致的前端重试:
try { $isDuplicate = Referral::where('mobile_phone', $request->phone) ->where('id_registration', $id_reg) ->exists(); if (!$isDuplicate) { $referral = new Referral; $referral->fullname = $request->fullname; $referral->mobile_phone = $request->phone; $referral->id_registration = $id_reg; // 其他字段赋值 $referral->save(); } Mail::send('mail.user.referral_creation', compact('email', 'fullname', 'user'), function ($message) use ($template, $email, $fullname, $main_sender_email, $main_sender_name) { $message->from($main_sender_email, $main_sender_name); $message->subject($template->greeting); $message->to($email, $fullname); }); return redirect('/')->with(['success' => 'success']); } catch (\Exception $e) { \Log::error($e); return redirect('/')->with(['error' => 'error']); }
(3)事务+悲观锁(可选,解决竞态问题)
如果担心高并发下的竞态问题,可使用事务加悲观锁确保查询和插入的原子性:
DB::transaction(function () use ($request, $id_reg) { $isDuplicate = Referral::where('mobile_phone', $request->phone) ->where('id_registration', $id_reg) ->lockForUpdate() ->exists(); if (!$isDuplicate) { $referral = new Referral; $referral->fullname = $request->fullname; $referral->mobile_phone = $request->phone; $referral->id_registration = $id_reg; // 其他字段赋值 $referral->save(); } });
内容的提问来源于stack exchange,提问作者sql query
相关产品推荐
相关产品推荐

