Laravel 乐观锁:高并发场景下的性能优化利器
本文将深入介绍一种更巧妙的并发控制机制,能让你的高并发应用性能飙升。无论是电商平台的库存扣减、社交应用的点赞计数,还是支付系统的余额更新,乐观锁都能发挥重要作用。
原文链接 Laravel 乐观锁:高并发场景下的性能优化利器
两个系统的故事
想象一下:高峰期到了,两家大型公司正在处理每秒数百万笔请求。
悲观系统采用了我们上篇文章讨论的传统方法------在做任何更改之前锁定每条记录。他们的数据库就像一座狭窄的桥,一次只能通过一辆车。安全吗?绝对的。快吗?不见得。
乐观系统则采用了完全不同的设计思路。他们的系统像一条多车道的高速公路,相信大多数时候车辆(请求)不会相撞。即便检测到潜在冲突,也能优雅地处理。
到下午 2 点,悲观系统的响应时间已经爬升到每笔请求 3 秒以上。而乐观系统仍然以 200ms 的平均响应时间稳定运行,处理的请求量是前者的 10 倍。
秘诀是什么?乐观锁。
什么是乐观锁?
乐观锁基于一个简单的前提:冲突很少发生,所以不必事先锁定一切。我们不在读取数据前获取锁,而是:
- 读取数据及其版本号
- 处理业务逻辑
- 仅在版本号未改变时才尝试更新
- 如果其他人同时修改了数据则重试
这就像编辑共享的 Google 文档------你不会在思考写什么的时候锁定整个文档。只有当别人在你工作时更改了同一部分,你才会收到警告。
真实场景:Maria 的午夜购物狂欢
来看看 Maria 的故事。她是个夜猫子,喜欢在午夜流量低的时候网购......至少她是这么认为的。
Maria 的乐观银行账户里有 1000 美元。在凌晨 12:00 整,她:
- 从科技商城订购了一台 400 美元的笔记本电脑
- 从活动中心购买了 300 美元的音乐会门票
- 从旅行公司预订了 350 美元的机票
这三笔交易在几毫秒内同时到达银行的 API。看看乐观锁如何完美解决这个问题:
Laravel 实现
让我们一步步构建,从数据库结构开始:
步骤 1:带版本列的数据库设置
php
// 迁移文件:为乐观锁添加版本列
Schema::table('accounts', function (Blueprint $table) {
$table->integer('version')->default(0)->after('balance');
$table->index(['account_number', 'version']);
});
步骤 2:带版本跟踪的 Account 模型
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Exceptions\OptimisticLockException;
class Account extends Model
{
protected $fillable = ['account_number', 'balance', 'version'];
protected $casts = [
'balance' => 'decimal:2'
];
/**
* 乐观锁:仅在版本匹配时更新
*/
public function updateBalanceOptimistically($newBalance, $expectedVersion = null)
{
$expectedVersion = $expectedVersion ?? $this->version;
$updated = $this->newQuery()
->where('id', $this->id)
->where('version', $expectedVersion)
->update([
'balance' => $newBalance,
'version' => $expectedVersion + 1,
'updated_at' => now()
]);
if (!$updated) {
throw new OptimisticLockException(
"账户 {$this->account_number} 已被其他交易修改"
);
}
// 更新模型实例
$this->balance = $newBalance;
$this->version = $expectedVersion + 1;
return $this;
}
}
步骤 3:自定义异常
php
<?php
namespace App\Exceptions;
class OptimisticLockException extends \Exception
{
public function __construct($message = "资源已被其他交易修改")
{
parent::__construct($message);
}
}
步骤 4:带重试逻辑的交易服务
php
<?php
namespace App\Services;
use App\Models\Account;
use App\Models\Transaction;
use App\Exceptions\OptimisticLockException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class OptimisticTransactionService
{
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 50; // 50 毫秒
public function transfer(string $fromAccount, string $toAccount, float $amount, string $reference)
{
return $this->executeWithRetry(function() use ($fromAccount, $toAccount, $amount, $reference) {
return DB::transaction(function() use ($fromAccount, $toAccount, $amount, $reference) {
// 获取两个账户的最新副本及当前版本
$sender = Account::where('account_number', $fromAccount)->first();
$receiver = Account::where('account_number', $toAccount)->first();
if (!$sender || !$receiver) {
throw new \Exception('账户不存在');
}
// 业务逻辑验证
if ($sender->balance < $amount) {
throw new \Exception('余额不足');
}
// 计算新余额
$newSenderBalance = $sender->balance - $amount;
$newReceiverBalance = $receiver->balance + $amount;
// 乐观更新(关键就在这里)
$sender->updateBalanceOptimistically($newSenderBalance);
$receiver->updateBalanceOptimistically($newReceiverBalance);
// 记录交易
$transaction = Transaction::create([
'from_account' => $fromAccount,
'to_account' => $toAccount,
'amount' => $amount,
'reference' => $reference,
'status' => 'completed',
'sender_version_used' => $sender->version - 1,
'receiver_version_used' => $receiver->version - 1,
]);
Log::info("转账完成", [
'transaction_id' => $transaction->id,
'from' => $fromAccount,
'to' => $toAccount,
'amount' => $amount
]);
return $transaction;
});
});
}
private function executeWithRetry(callable $operation, int $attempt = 1)
{
try {
return $operation();
} catch (OptimisticLockException $e) {
if ($attempt >= self::MAX_RETRIES) {
Log::error("乐观锁重试次数超限", [
'attempts' => $attempt,
'error' => $e->getMessage()
]);
throw new \Exception("交易繁忙,请稍后重试");
}
Log::info("乐观锁冲突,正在重试", [
'attempt' => $attempt,
'next_attempt_in_ms' => self::RETRY_DELAY_MS * $attempt
]);
// 指数退避:每次重试等待更长时间
usleep(self::RETRY_DELAY_MS * $attempt * 1000);
return $this->executeWithRetry($operation, $attempt + 1);
}
}
}
步骤 5:API 控制器
php
<?php
namespace App\Http\Controllers;
use App\Services\OptimisticTransactionService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class TransferController extends Controller
{
public function __construct(
private OptimisticTransactionService $transactionService
) {}
public function transfer(Request $request): JsonResponse
{
$validated = $request->validate([
'from_account' => 'required|string',
'to_account' => 'required|string',
'amount' => 'required|numeric|min:0.01',
'reference' => 'required|string|max:255'
]);
try {
$transaction = $this->transactionService->transfer(
$validated['from_account'],
$validated['to_account'],
$validated['amount'],
$validated['reference']
);
return response()->json([
'success' => true,
'transaction_id' => $transaction->id,
'message' => '转账成功'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 400);
}
}
}
回到 Maria 的故事:整个流程如何工作
当 Maria 的三笔交易同时到达系统时:
- 交易 A(笔记本电脑):读取 Maria 的账户(余额:$1000,版本:5)
- 交易 B(音乐会):同样读取账户(余额:$1000,版本:5)
- 交易 C(机票):同样的操作(余额:$1000,版本:5)
精彩的来了:
- 交易 A 首先完成:更新余额为 $600,版本为 6
- 交易 B 尝试更新:版本不匹配!(期望 5,实际 6)
- 交易 B 重试:读取最新数据(余额:$600,版本:6),成功完成
- 交易 C 尝试更新:又一次版本不匹配,使用最新数据重试
- 交易 C 失败:余额不足(余额:300,需要:350)
结果:Maria 成功购买了笔记本电脑和音乐会门票,但机票预订失败,并收到清晰的错误消息。没有资金损失,没有数据不一致,所有这些都在 500ms 内完成!
性能优势:数字说话
在实际的高并发系统中,从悲观锁切换到乐观锁往往能带来显著提升:
- 5 倍的交易吞吐量提升
- **60%**的平均响应时间缩减
- **90%**的数据库连接超时减少
- 零数据一致性问题
秘密武器?大多数交易其实并不冲突。避免不必要的锁,数据库就能腾出手来处理更多并发操作。
何时使用乐观锁
最佳应用场景:
- 读多写少、冲突少的场景(如文章阅读计数、商品浏览量)
- 电商系统的库存扣减(并发购买同一商品)
- 社交应用的点赞、收藏功能
- 用户积分、钱包余额的更新
- 性能至关重要的高并发系统
- 具有良好重试机制的应用
不适用于:
- 冲突频繁的场景(>10% 的操作)
- 重试逻辑会影响用户体验的情况
- 需要保证立即一致性的系统
- 网络延迟导致重试成本过高的环境
实战高级技巧
1. 智能版本列
php
// 使用时间戳而不是简单整数,便于更好的调试
Schema::table('accounts', function (Blueprint $table) {
$table->timestamp('version')->default(DB::raw('CURRENT_TIMESTAMP(6)'));
});
2. 冲突指标监控
php
// 添加监控以了解冲突率
class OptimisticLockMetrics
{
public static function recordConflict($model, $operation)
{
Cache::increment("optimistic_conflicts:{$model}:{$operation}:".now()->format('Y-m-d-H'));
}
}
3. 优雅降级
php
// 在高冲突期间回退到悲观锁
if ($this->getConflictRate() > 0.1) {
return $this->pessimisticTransfer($fromAccount, $toAccount, $amount);
}
总结
乐观锁不仅是个数据库技巧------更是一种思维方式的转变。不再一上来就锁定一切,而是采用更聪明的策略:只在冲突真正发生时才去处理。
在高并发的互联网世界中,每一毫秒的延迟都会影响用户体验,每一次失败的请求都可能流失用户。乐观锁为我们提供了两全其美的方案:极致的性能和坚如磐石的一致性。
无论你构建的是电商平台、社交应用还是支付系统,当面对高并发场景时,请记住 Maria 的故事。有时候,保持乐观不仅有益于心理健康------对应用性能同样大有裨益。