Laravel中如何为用户锁表/避免路由并行重复调用
解决并行请求导致重复交易记录的问题
问题根源
两次并行请求同时执行查询逻辑时,都能获取到generated_until小于当前时间的交易记录,随后各自执行插入操作,最终生成重复数据。
解决方案
方案一:数据库事务+悲观行锁(单服务器场景首选)
利用Laravel的数据库事务和lockForUpdate()行级锁,在查询阶段锁住目标记录,阻止其他并行请求读取这些记录,直到当前事务完成并释放锁。
修改init方法如下:
public function init() { DB::beginTransaction(); try { // 锁住符合条件的交易行,其他请求需等待锁释放才能查询 $transactions = Transaction::where('generated_until', '<', Carbon::now()) ->lockForUpdate() ->get(); $transactions->each(function ($transaction) { Auth::user()->transactions()->create([ // 填充需要的字段 'generated_until' => Carbon::now()->addMonth() ]); // 同步更新原交易的generated_until,彻底避免后续被重复选中 $transaction->update(['generated_until' => Carbon::now()->addMonth()]); }); DB::commit(); } catch (\Exception $e) { DB::rollBack(); return response()->json(['error' => '操作失败:' . $e->getMessage()], 500); } return response()->json(['message' => '初始化完成']); }
原理:lockForUpdate()会在事务内对查询到的行加排他锁,并行请求执行到相同查询时会被阻塞,直到当前事务提交锁释放。此时原交易的generated_until已被更新为未来时间,第二个请求查询时不会再选中这些记录,自然不会重复插入。
方案二:Redis分布式锁(多服务器部署场景)
如果服务部署在多台服务器上,数据库行锁无法跨服务器生效,这时候可以用Redis实现分布式锁,确保同一用户的初始化操作同一时间只有一个请求在执行。
代码示例:
use Illuminate\Support\Facades\Redis; public function init() { $userId = Auth::id(); // 生成针对当前用户的唯一锁键 $lockKey = "transaction_init_lock_{$userId}"; // 尝试获取锁,设置60秒有效期(根据实际操作耗时调整) $lockAcquired = Redis::set($lockKey, true, 'EX', 60, 'NX'); if (!$lockAcquired) { return response()->json(['message' => '操作正在处理中,请稍后再试'], 429); } try { $transactions = Transaction::where('generated_until', '<', Carbon::now())->get(); $transactions->each(function ($transaction) { Auth::user()->transactions()->create([ // 填充字段 'generated_until' => Carbon::now()->addMonth() ]); }); } catch (\Exception $e) { Redis::del($lockKey); return response()->json(['error' => '操作失败:' . $e->getMessage()], 500); } // 操作完成后释放锁 Redis::del($lockKey); return response()->json(['message' => '初始化完成']); }
原理:通过Redis的原子性set命令获取锁,只有第一个请求能拿到锁并执行逻辑,后续请求直接返回“处理中”提示,避免并行操作。
方案三:数据库唯一约束(兜底保障)
不管用哪种锁机制,都建议给交易表加唯一约束,从数据库层面阻止重复数据插入,作为最后一道防线。
比如在迁移文件中添加唯一索引(根据业务逻辑调整字段组合,确保能唯一标识一条交易):
Schema::table('transactions', function (Blueprint $table) { // 示例:结合用户ID、原交易ID、生成截止时间作为唯一约束 $table->unique(['user_id', 'original_transaction_id', 'generated_until']); });
这样即使锁机制失效,数据库会抛出唯一约束冲突的异常,不会插入重复记录,你可以在代码中捕获该异常并返回友好提示。
内容的提问来源于stack exchange,提问作者Frnak
相关产品推荐
相关产品推荐

