Laravel乐观锁怎么用

wen PHP项目 4

本文目录导读:

Laravel乐观锁怎么用

  1. 使用 Eloquent 内置特性
  2. 手动实现乐观锁
  3. 使用 trait 封装乐观锁
  4. 使用数据库事务优化
  5. 使用 Laravel 辅助函数
  6. 完整示例:API 控制器
  7. 前端配合示例
  8. 注意事项

在 Laravel 中使用乐观锁(Optimistic Locking)主要通过版本号机制实现,以下是几种常见的使用方法:

使用 Eloquent 内置特性

模型准备

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
    protected $fillable = ['name', 'price', 'stock'];
}

迁移文件添加版本字段

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->decimal('price', 10, 2);
        $table->integer('stock');
        $table->integer('version')->default(1); // 乐观锁版本字段
        $table->timestamps();
    });
}

手动实现乐观锁

基础实现方法

use App\Models\Product;
use Illuminate\Database\Eloquent\ModelNotFoundException;
public function updateStock($productId, $quantity)
{
    // 1. 查询当前版本号
    $product = Product::findOrFail($productId);
    $currentVersion = $product->version;
    try {
        // 2. 带版本条件更新
        $updated = Product::where('id', $productId)
            ->where('version', $currentVersion)
            ->update([
                'stock' => $product->stock - $quantity,
                'version' => $currentVersion + 1
            ]);
        // 3. 检查是否更新成功
        if ($updated === 0) {
            throw new \Exception('数据已被其他用户修改,请刷新重试');
        }
        return true;
    } catch (\Exception $e) {
        // 处理并发冲突
        Log::error('乐观锁更新失败: ' . $e->getMessage());
        return false;
    }
}

使用 trait 封装乐观锁

创建 Trait

<?php
namespace App\Traits;
trait OptimisticLocking
{
    protected string $versionColumn = 'version';
    public function getVersionColumn(): string
    {
        return $this->versionColumn ?? 'version';
    }
    public function incrementVersion(): void
    {
        $this->increment($this->getVersionColumn());
    }
    public function updateWithLock(array $data): bool
    {
        $currentVersion = $this->{$this->getVersionColumn()};
        $updated = static::where($this->getKeyName(), $this->getKey())
            ->where($this->getVersionColumn(), $currentVersion)
            ->update(array_merge($data, [
                $this->getVersionColumn() => $currentVersion + 1
            ]));
        if ($updated === 0) {
            throw new \Exception('乐观锁更新失败,数据已被修改');
        }
        return true;
    }
}

在模型中使用

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\OptimisticLocking;
class Product extends Model
{
    use OptimisticLocking;
    protected $fillable = ['name', 'price', 'stock'];
    protected string $versionColumn = 'version';
}

使用数据库事务优化

结合事务的乐观锁

use Illuminate\Support\Facades\DB;
use App\Models\Order;
public function createOrder($productId, $userId, $quantity)
{
    DB::beginTransaction();
    try {
        $product = Product::findOrFail($productId);
        $currentVersion = $product->version;
        // 检查库存
        if ($product->stock < $quantity) {
            throw new \Exception('库存不足');
        }
        // 带版本号的更新
        $updated = Product::where('id', $productId)
            ->where('version', $currentVersion)
            ->update([
                'stock' => $product->stock - $quantity,
                'version' => $currentVersion + 1
            ]);
        if ($updated === 0) {
            throw new \Exception('并发冲突,请重试');
        }
        // 创建订单记录
        Order::create([
            'user_id' => $userId,
            'product_id' => $productId,
            'quantity' => $quantity
        ]);
        DB::commit();
        return true;
    } catch (\Exception $e) {
        DB::rollBack();
        throw $e;
    }
}

使用 Laravel 辅助函数

使用 updateOrCreate 方法

$product = Product::updateOrCreate(
    ['id' => $productId, 'version' => $currentVersion],
    [
        'stock' => $newStock,
        'version' => $currentVersion + 1
    ]
);

完整示例:API 控制器

<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ProductController extends Controller
{
    public function purchase(Request $request, $productId)
    {
        $request->validate([
            'quantity' => 'required|integer|min:1',
            'version' => 'required|integer'
        ]);
        try {
            $product = Product::findOrFail($productId);
            $quantity = $request->input('quantity');
            $clientVersion = $request->input('version');
            // 检查客户端版本是否匹配
            if ($clientVersion !== $product->version) {
                return response()->json([
                    'success' => false,
                    'message' => '数据已过期,请刷新页面',
                    'current_version' => $product->version
                ], 409); // 409 Conflict
            }
            // 检查库存
            if ($product->stock < $quantity) {
                return response()->json([
                    'success' => false,
                    'message' => '库存不足,当前库存:' . $product->stock
                ], 400);
            }
            // 执行更新
            DB::beginTransaction();
            $updated = Product::where('id', $productId)
                ->where('version', $clientVersion)
                ->update([
                    'stock' => $product->stock - $quantity,
                    'version' => $clientVersion + 1
                ]);
            if ($updated === 0) {
                throw new \Exception('乐观锁冲突');
            }
            DB::commit();
            // 返回更新后的数据
            $product->refresh();
            return response()->json([
                'success' => true,
                'data' => [
                    'stock' => $product->stock,
                    'version' => $product->version
                ],
                'message' => '购买成功'
            ]);
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error('购买失败', [
                'product_id' => $productId,
                'error' => $e->getMessage()
            ]);
            return response()->json([
                'success' => false,
                'message' => '操作失败,请重试'
            ], 500);
        }
    }
}

前端配合示例

// 前端请求示例
async function purchaseProduct(productId, quantity) {
    // 先获取当前版本号
    const product = await fetch(`/api/products/${productId}`).then(r => r.json());
    try {
        const response = await fetch(`/api/products/${productId}/purchase`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
            },
            body: JSON.stringify({
                quantity: quantity,
                version: product.version  // 传递当前版本号
            })
        });
        const result = await response.json();
        if (!result.success) {
            if (response.status === 409) {
                // 版本冲突,提示用户刷新
                alert('数据已过期,请刷新页面');
                window.location.reload();
            } else {
                alert(result.message);
            }
            return;
        }
        // 更新成功
        console.log('购买成功', result.data);
    } catch (error) {
        console.error('请求失败:', error);
    }
}

注意事项

  1. 版本字段建议设置为整数类型,便于比较和自增
  2. 每次更新必须同时更新版本号,否则乐观锁会失效
  3. 事务中要捕获乐观锁异常,确保回滚
  4. 前端也要配合传递当前版本号,形成完整闭环
  5. 高并发场景下,建议结合数据库索引和连接池优化

乐观锁适用于读多写少的场景,可以有效解决并发更新问题,但会增加额外的版本检查开销。

抱歉,评论功能暂时关闭!