Yii 2: The Fast, Secure and Professional PHP Framework 数据库并发控制:乐观锁实现
在多用户同时操作同一数据时,您是否曾遇到过数据被意外覆盖的问题?例如电商平台中两个用户同时购买最后一件商品,或库存系统中多人同时调整同一产品数量。这些场景下,普通的更新操作可能导致数据不一致。Yii 2 提供的乐观锁(Optimistic Lock)机制能高效解决此类并发冲突,无需复杂的数据库锁机制,即可确保数据一致性。本文将通过实际案例,详细讲解 Yii 2 乐观锁的实现步骤与最佳实践。
什么是乐观锁?
乐观锁(Optimistic Lock)是一种并发控制策略,它假设多用户同时操作同一数据的概率较低,因此不会主动加锁,而是通过版本号机制检测冲突。当数据更新时,系统会检查版本号是否匹配:若匹配则允许更新并递增版本号;若不匹配则说明数据已被其他用户修改,此时抛出异常阻止更新。
与悲观锁(如数据库的 SELECT ... FOR UPDATE)相比,乐观锁具有以下优势:
- 性能更高:无需数据库锁等待,减少系统开销
- 无死锁风险:避免长时间持有锁导致的死锁问题
- 适用高频读场景:特别适合读多写少的业务场景
Yii 2 乐观锁实现步骤
Yii 2 的 Active Record 组件内置了乐观锁支持,实现过程只需四个关键步骤:
1. 数据库表设计
首先在数据表中添加版本控制字段,建议使用 BIGINT 类型并默认值为 0。以订单表为例:
CREATE TABLE `order` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`product_id` INT NOT NULL,
`quantity` INT NOT NULL DEFAULT 1,
`version` BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
2. 模型配置
在对应的 Active Record 模型中,需完成两项配置:重写 optimisticLock() 方法指定版本字段名,并附加 OptimisticLockBehavior 行为处理版本验证。
<?php
// models/Order.php
namespace app\models;
use yii\db\ActiveRecord;
use yii\behaviors\OptimisticLockBehavior;
class Order extends ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'order';
}
/**
* {@inheritdoc}
*/
public function behaviors()
{
return array_merge(parent::behaviors(), [
OptimisticLockBehavior::class,
]);
}
/**
* 声明乐观锁版本字段
* {@inheritdoc}
*/
public function optimisticLock()
{
return 'version';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['product_id', 'quantity'], 'required'],
[['product_id', 'quantity'], 'integer'],
// 注意:版本字段无需添加验证规则,由 OptimisticLockBehavior 处理
];
}
}
官方文档:乐观锁实现
3. 表单实现
在更新数据的表单中,添加隐藏字段存储当前版本号。当用户提交表单时,该版本号会一同发送到服务器进行验证。
<?php
// views/order/update.php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model app\models\Order */
?>
<div class="order-form">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'product_id')->textInput() ?>
<?= $form->field($model, 'quantity')->textInput() ?>
<!-- 乐观锁版本号隐藏字段 -->
<?= Html::activeHiddenInput($model, 'version') ?>
<div class="form-group">
<?= Html::submitButton('保存', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
4. 控制器冲突处理
在控制器的更新操作中,需要捕获 yii\db\StaleObjectException 异常,该异常会在版本号不匹配时抛出。捕获异常后,可根据业务需求实现冲突解决策略(如提示用户刷新页面、自动合并更改等)。
<?php
// controllers/OrderController.php
namespace app\controllers;
use Yii;
use app\models\Order;
use yii\db\StaleObjectException;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
class OrderController extends Controller
{
/**
* 更新订单
* @param int $id
* @return mixed
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);
try {
if ($model->load(Yii::$app->request->post()) && $model->save()) {
Yii::$app->session->setFlash('success', '订单更新成功!');
return $this->redirect(['view', 'id' => $model->id]);
}
} catch (StaleObjectException $e) {
Yii::$app->session->setFlash('error', '数据已被其他用户修改,请刷新页面后重试!');
return $this->refresh();
}
return $this->render('update', [
'model' => $model,
]);
}
/**
* 查找模型
* @param int $id
* @return Order
* @throws NotFoundHttpException
*/
protected function findModel($id)
{
if (($model = Order::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('请求的页面不存在。');
}
}
乐观锁工作原理
Yii 2 乐观锁的核心机制是通过版本号比对实现的。当执行 save() 操作时,Active Record 会自动执行以下步骤:
-
版本号验证:生成的 SQL 语句会包含版本号条件,例如:
UPDATE `order` SET `quantity`=2, `version`=1 WHERE `id`=1 AND `version`=0 -
冲突检测:如果更新影响行数为 0(即版本号不匹配),系统会抛出
yii\db\StaleObjectException异常。 -
版本号更新:如果验证通过,版本号会自动递增,确保下一次更新时能检测到冲突。
高级应用场景
批量更新处理
对于批量更新场景,可通过事务结合乐观锁确保数据一致性:
use yii\db\Transaction;
$transaction = Yii::$app->db->beginTransaction();
try {
$order1 = Order::findOne(1);
$order1->quantity += 1;
$order1->save();
$order2 = Order::findOne(2);
$order2->quantity += 1;
$order2->save();
$transaction->commit();
} catch (StaleObjectException $e) {
$transaction->rollBack();
Yii::$app->session->setFlash('error', '批量更新失败:数据已被修改');
}
自定义冲突解决策略
对于复杂业务场景,可实现更智能的冲突解决逻辑,例如自动合并非冲突字段的更改:
catch (StaleObjectException $e) {
// 获取数据库最新版本
$latestModel = $this->findModel($model->id);
// 合并非冲突字段(这里以 quantity 为例)
if ($model->quantity != $latestModel->quantity) {
// 实现自定义合并逻辑,例如取较大值
$latestModel->quantity = max($model->quantity, $latestModel->quantity);
$latestModel->save();
Yii::$app->session->setFlash('success', '检测到冲突,已自动合并更改');
return $this->redirect(['view', 'id' => $latestModel->id]);
}
// 无法自动解决的冲突,提示用户
Yii::$app->session->setFlash('error', '数据已被修改,请确认后重试');
return $this->render('update', [
'model' => $latestModel,
]);
}
注意事项
-
适用场景:乐观锁适用于读多写少的场景,若冲突频率高(如秒杀系统),建议使用悲观锁或分布式锁。
-
版本字段类型:必须使用数值类型(如
BIGINT),不支持字符串类型。 -
批量操作:
updateAll()、deleteAll()等批量操作不会触发乐观锁验证,需手动实现版本控制。 -
行为依赖:
OptimisticLockBehavior自 Yii 2.0.16 版本引入,低版本需手动实现版本验证逻辑。
总结
Yii 2 的乐观锁机制通过简洁的 API 设计,让开发者能轻松处理并发冲突问题。实现过程只需四步:添加版本字段、配置模型、更新表单、处理冲突异常。这种轻量级方案在大多数业务场景下能有效保证数据一致性,同时避免了悲观锁带来的性能开销。
在实际项目中,建议结合业务特点选择合适的并发控制策略:普通表单更新使用乐观锁,高冲突场景使用悲观锁,分布式系统考虑 Redis 锁等方案。
相关源码:OptimisticLockBehavior 官方文档:Active Record 并发控制
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



