本文目录导读:

在 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);
}
}
注意事项
- 版本字段建议设置为整数类型,便于比较和自增
- 每次更新必须同时更新版本号,否则乐观锁会失效
- 事务中要捕获乐观锁异常,确保回滚
- 前端也要配合传递当前版本号,形成完整闭环
- 高并发场景下,建议结合数据库索引和连接池优化
乐观锁适用于读多写少的场景,可以有效解决并发更新问题,但会增加额外的版本检查开销。