本文目录导读:

在PHP项目中实现乐观锁,通常有两种主流方式:基于版本号 和 基于时间戳,下面详细介绍这两种方式,并提供完整的代码示例。
基于版本号的乐观锁(推荐)
这是最常用、最可靠的方式,通过在数据表中增加一个 version 字段来实现。
数据库表设计
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
stock INT NOT NULL,
version INT NOT NULL DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 插入测试数据
INSERT INTO products (name, stock, version) VALUES ('iPhone 15', 100, 0);
PHP 实现代码
<?php
class OptimisticLockService
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 使用乐观锁更新库存
*
* @param int $productId 商品ID
* @param int $quantity 要扣减的数量
* @return bool 是否成功
* @throws RuntimeException
*/
public function updateStockWithOptimisticLock(int $productId, int $quantity): bool
{
// 1. 读取当前版本号
$stmt = $this->pdo->prepare("SELECT stock, version FROM products WHERE id = ?");
$stmt->execute([$productId]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
throw new RuntimeException("商品不存在");
}
// 2. 检查库存是否充足
if ($product['stock'] < $quantity) {
throw new RuntimeException("库存不足");
}
$currentVersion = $product['version'];
$newStock = $product['stock'] - $quantity;
// 3. 尝试更新,版本号作为条件
$updateStmt = $this->pdo->prepare(
"UPDATE products
SET stock = ?, version = version + 1
WHERE id = ? AND version = ?"
);
$updateStmt->execute([$newStock, $productId, $currentVersion]);
// 4. 检查受影响的行数
if ($updateStmt->rowCount() === 0) {
throw new RuntimeException("并发冲突,数据已被其他用户修改");
}
return true;
}
/**
* 带重试机制的更新
*
* @param int $productId
* @param int $quantity
* @param int $maxRetries 最大重试次数
* @return bool
*/
public function updateStockWithRetry(int $productId, int $quantity, int $maxRetries = 3): bool
{
$attempts = 0;
while ($attempts < $maxRetries) {
try {
return $this->updateStockWithOptimisticLock($productId, $quantity);
} catch (RuntimeException $e) {
if ($e->getMessage() !== "并发冲突,数据已被其他用户修改") {
throw $e; // 非并发错误,直接抛出
}
$attempts++;
if ($attempts >= $maxRetries) {
throw new RuntimeException("多次重试后仍然失败,请稍后再试");
}
// 随机延迟,避免冲突
usleep(rand(100000, 500000)); // 100-500ms
}
}
return false;
}
}
// 使用示例
try {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$lockService = new OptimisticLockService($pdo);
// 尝试扣减库存
$result = $lockService->updateStockWithOptimisticLock(1, 2);
if ($result) {
echo "库存更新成功";
}
} catch (RuntimeException $e) {
echo "错误: " . $e->getMessage();
}
基于时间戳的乐观锁
使用 updated_at 字段作为乐观锁的判断条件。
<?php
class TimestampOptimisticLock
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 基于时间戳的乐观锁更新
*/
public function updateWithTimestamp(int $id, array $updateData): bool
{
// 1. 先读取数据
$stmt = $this->pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
throw new RuntimeException("记录不存在");
}
$lastUpdatedAt = $product['updated_at'];
// 2. 构建更新语句,条件包含时间戳
$fields = [];
$params = [];
foreach ($updateData as $field => $value) {
$fields[] = "$field = ?";
$params[] = $value;
}
$params[] = $id; // WHERE id
$params[] = $lastUpdatedAt; // AND updated_at
$sql = "UPDATE products
SET " . implode(', ', $fields) . "
WHERE id = ? AND updated_at = ?";
$updateStmt = $this->pdo->prepare($sql);
$updateStmt->execute($params);
if ($updateStmt->rowCount() === 0) {
throw new RuntimeException("数据已被修改,请刷新后重试");
}
return true;
}
}
使用 Laravel 框架的乐观锁
如果你使用 Laravel,可以更优雅地实现:
安装扩展包
composer require genealabs/laravel-optimistic-locking
模型中使用
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use GeneaLabs\LaravelOptimisticLocking\Traits\OptimisticLocking;
class Product extends Model
{
use OptimisticLocking;
protected $fillable = ['name', 'stock'];
// 指定乐观锁字段
protected $optimisticLockColumn = 'version';
}
手动实现乐观锁(不依赖扩展)
<?php
namespace App\Services;
use App\Models\Product;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class OptimisticLockService
{
public function updateStock(int $productId, int $quantity)
{
return DB::transaction(function () use ($productId, $quantity) {
// 读取当前数据
$product = Product::findOrFail($productId);
// 保存版本号
$originalVersion = $product->version;
// 执行业务逻辑
$product->stock -= $quantity;
// 使用乐观锁更新
$affected = Product::where('id', $productId)
->where('version', $originalVersion)
->update([
'stock' => $product->stock,
'version' => DB::raw('version + 1'),
]);
if ($affected === 0) {
throw ValidationException::withMessages([
'stock' => '并发冲突,请刷新后重试',
]);
}
return $product->fresh();
});
}
}
前端配合实现乐观锁
传递版本号
// 前端请求时携带版本号
async function updateStock(productId, quantity, version) {
const response = await fetch('/api/update-stock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
quantity: quantity,
version: version // 传递当前版本号
})
});
const result = await response.json();
if (!result.success) {
if (result.error === 'conflict') {
// 发生冲突,提示用户刷新
alert('数据已被其他用户修改,正在刷新...');
window.location.reload();
}
}
}
后端验证
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProductController extends Controller
{
public function updateStock(Request $request)
{
$request->validate([
'product_id' => 'required|integer',
'quantity' => 'required|integer|min:1',
'version' => 'required|integer',
]);
$product = Product::findOrFail($request->product_id);
// 验证版本号
if ($product->version !== (int)$request->version) {
return response()->json([
'success' => false,
'error' => 'conflict',
'message' => '数据已被修改'
], 409);
}
// 执行更新...
}
}
最佳实践建议
选择合适的锁粒度
- 行级乐观锁:适用于单行更新
- 表级乐观锁:适用于批量操作
处理冲突策略
/**
* 冲突处理策略
*/
interface ConflictResolutionStrategy
{
public function resolve(int $retryCount): void;
}
class RetryStrategy implements ConflictResolutionStrategy
{
public function resolve(int $retryCount): void
{
// 指数退避
$delay = min(1000 * pow(2, $retryCount), 10000); // 最大10秒
usleep($delay * 1000);
}
}
class NotifyUserStrategy implements ConflictResolutionStrategy
{
public function resolve(int $retryCount): void
{
throw new RuntimeException("操作冲突,请重新尝试");
}
}
性能优化
/**
* 批量更新使用乐观锁
*/
function batchUpdateWithOptimisticLock(array $items): void
{
$caseStatements = [];
$ids = [];
foreach ($items as $item) {
$ids[] = $item['id'];
$caseStatements[] = "WHEN id = {$item['id']} AND version = {$item['version']} THEN version + 1";
}
$sql = "UPDATE products
SET stock = CASE
" . implode(' ', $caseStatements) . "
ELSE stock
END,
version = CASE
" . implode(' ', $caseStatements) . "
ELSE version
END
WHERE id IN (" . implode(',', $ids) . ")";
// 执行批量更新...
}
注意事项
- 索引优化:确保
version字段有索引,避免全表扫描 - 事务范围:乐观锁应配合事务使用,保持数据一致性
- 重试机制:建议实现指数退避的重试策略
- 监控告警:当冲突率过高时,考虑是否需要改用悲观锁
通过以上实现,PHP项目中的乐观锁可以有效解决并发更新问题,特别适合读多写少、冲突概率较低的场景。