You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 22:08:17