本文目录导读:

在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项目中的乐观锁可以有效解决并发更新问题,特别适合读多写少、冲突概率较低的场景。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播