Yii 2: The Fast, Secure and Professional PHP Framework 数据库并发控制:乐观锁实现

Yii 2: The Fast, Secure and Professional PHP Framework 数据库并发控制:乐观锁实现

【免费下载链接】yii2 Yii 2: The Fast, Secure and Professional PHP Framework 【免费下载链接】yii2 项目地址: https://gitcode.com/gh_mirrors/yi/yii2

在多用户同时操作同一数据时,您是否曾遇到过数据被意外覆盖的问题?例如电商平台中两个用户同时购买最后一件商品,或库存系统中多人同时调整同一产品数量。这些场景下,普通的更新操作可能导致数据不一致。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 会自动执行以下步骤:

  1. 版本号验证:生成的 SQL 语句会包含版本号条件,例如:

    UPDATE `order` SET `quantity`=2, `version`=1 WHERE `id`=1 AND `version`=0
    
  2. 冲突检测:如果更新影响行数为 0(即版本号不匹配),系统会抛出 yii\db\StaleObjectException 异常。

  3. 版本号更新:如果验证通过,版本号会自动递增,确保下一次更新时能检测到冲突。

高级应用场景

批量更新处理

对于批量更新场景,可通过事务结合乐观锁确保数据一致性:

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,
    ]);
}

注意事项

  1. 适用场景:乐观锁适用于读多写少的场景,若冲突频率高(如秒杀系统),建议使用悲观锁或分布式锁。

  2. 版本字段类型:必须使用数值类型(如 BIGINT),不支持字符串类型。

  3. 批量操作updateAll()deleteAll() 等批量操作不会触发乐观锁验证,需手动实现版本控制。

  4. 行为依赖OptimisticLockBehavior 自 Yii 2.0.16 版本引入,低版本需手动实现版本验证逻辑。

总结

Yii 2 的乐观锁机制通过简洁的 API 设计,让开发者能轻松处理并发冲突问题。实现过程只需四步:添加版本字段、配置模型、更新表单、处理冲突异常。这种轻量级方案在大多数业务场景下能有效保证数据一致性,同时避免了悲观锁带来的性能开销。

在实际项目中,建议结合业务特点选择合适的并发控制策略:普通表单更新使用乐观锁,高冲突场景使用悲观锁,分布式系统考虑 Redis 锁等方案。

相关源码:OptimisticLockBehavior 官方文档:Active Record 并发控制

【免费下载链接】yii2 Yii 2: The Fast, Secure and Professional PHP Framework 【免费下载链接】yii2 项目地址: https://gitcode.com/gh_mirrors/yi/yii2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值