怎样在PHP项目中实现乐观锁?

wen java案例 2

本文目录导读:

怎样在PHP项目中实现乐观锁?

  1. 基于版本号的乐观锁(推荐)
  2. 基于时间戳的乐观锁
  3. 使用 Laravel 框架的乐观锁
  4. 前端配合实现乐观锁
  5. 最佳实践建议
  6. 注意事项

在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) . ")";
    // 执行批量更新...
}

注意事项

  1. 索引优化:确保 version 字段有索引,避免全表扫描
  2. 事务范围:乐观锁应配合事务使用,保持数据一致性
  3. 重试机制:建议实现指数退避的重试策略
  4. 监控告警:当冲突率过高时,考虑是否需要改用悲观锁

通过以上实现,PHP项目中的乐观锁可以有效解决并发更新问题,特别适合读多写少、冲突概率较低的场景。

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