Maison  >  Article  >  développement back-end  >  Explication détaillée des exemples de verrouillage optimiste et pessimiste dans Yii2.0

Explication détaillée des exemples de verrouillage optimiste et pessimiste dans Yii2.0

小云云
小云云original
2018-02-01 11:14:192302parcourir

Les applications Web sont souvent confrontées à des environnements multi-utilisateurs. Dans ce cas, le contrôle d'écriture simultané est devenu une compétence que presque tous les développeurs doivent maîtriser. Cet article présente principalement les principes et les utilisations du verrouillage optimiste et pessimiste dans Yii2.0. J'espère qu'il pourra vous aider.

Dans un environnement simultané, des situations telles qu'une lecture sale, une lecture non répétable, une lecture fantôme, une mise à jour perdue, etc. peuvent survenir. Vous pouvez rechercher vous-même des performances spécifiques.

Afin de résoudre ces problèmes, les bases de données traditionnelles fournissent des mécanismes de verrouillage et introduisent le concept de niveau d'isolation des transactions. Nous ne l’expliquerons pas ici. Recherchez simplement ces mots-clés et vous en trouverez beaucoup sur Internet.

Cependant, en ce qui concerne le processus de développement spécifique, il existe généralement deux méthodes pour résoudre le problème des conflits de concurrence : le verrouillage pessimiste et le verrouillage optimiste.

Verrouillage optimiste

Le verrouillage optimiste montre une attitude audacieuse et pragmatique. Le principe de l'utilisation du verrouillage optimiste est que dans les applications réelles, la probabilité de conflit est relativement faible. Sa conception et sa mise en œuvre sont directes et concises. Dans les applications Web actuelles, l'utilisation du verrouillage optimiste présente un avantage absolu.

Par conséquent, Yii fournit également un support de verrouillage optimiste pour ActiveReocrd.

Selon la documentation officielle de Yii, l'utilisation du verrouillage optimiste est divisée en 4 étapes :

  • Ajouter un champ à la table qui doit être verrouillé pour représenter le numéro de version. Bien entendu, le modèle correspondant doit également être ajouté au champ et procéder aux ajustements appropriés. Par exemple, ce champ doit être ajouté à Rules().

  • Surchargez la méthode yiidbActiveRecord::optimisticLock() pour renvoyer le nom du champ à l'étape précédente.

  • Dans le formulaire de la page de modification de l'enregistrement, ajoutez un 10a0c4c6fe334f1dd2642c4aa224944a pour stocker temporairement le numéro de version de l'enregistrement lors de la lecture.

  • Là où vous enregistrez le code, utilisez try ... catch pour voir si vous pouvez intercepter une exception yiidbStaleObjectException. Si tel est le cas, cela signifie que l'enregistrement a été modifié lors de cette modification de l'enregistrement. S'il s'agit d'une réponse simple, vous pouvez donner les invites correspondantes. Si vous êtes intelligent, vous pouvez fusionner les modifications non conflictuelles ou afficher une page de comparaison.

Essentiellement parlant, le verrouillage optimiste n'utilise pas le mécanisme de verrouillage de la base de données comme le verrouillage pessimiste. Le verrouillage optimiste ajoute un champ de comptage au tableau pour représenter le nombre de fois que l'enregistrement actuel a été modifié (numéro de version).

Ensuite, implémentez le verrouillage optimiste en comparant les numéros de version avant de mettre à jour ou de supprimer.

Déclarez le champ du numéro de version

Le numéro de version est la base de la mise en œuvre du verrouillage optimiste. La première étape consiste donc à indiquer à Yii quel champ est le champ du numéro de version. Ceci est géré par yiidbBaseActiveRecord :


public function optimisticLock()
{
  return null;
}

Cette méthode renvoie null , indiquant que le verrouillage optimiste n'est pas utilisé. Ensuite, nous devons surcharger cela dans notre modèle. Renvoie une chaîne représentant le champ que nous avons utilisé pour identifier le numéro de version. Par exemple, cela peut ressembler à ceci :


public function optimisticLock()
{
  return 'ver';
}

Cela signifie qu'il y a un champ ver dans l'ActiveRecord actuel, qui peut être utilisé pour le verrouillage optimiste. Alors, comment Yii utilise-t-il ce champ ver pour implémenter le verrouillage optimiste ?

Processus de mise à jour

Plus précisément, le processus de mise à jour après l'utilisation du verrouillage optimiste est le suivant :

  1. Lire Obtenir l'enregistrement à mettre à jour.

  2. Modifier la fiche selon les souhaits de l'utilisateur. Bien entendu, le champ ver ne sera pas modifié pour le moment. Ce champ n'a aucun sens pour l'utilisateur.

  3. Avant de sauvegarder l'enregistrement, relisez le champ ver de l'enregistrement et comparez-le avec la valeur lue précédemment.

  4. Si ver est différent, cela signifie que cet enregistrement a été modifié par d'autres lors du processus de modification de l'utilisateur. Nous allons donc vous donner un indice.

  5. Si ver est le même, cela signifie que cet enregistrement n'a pas été modifié. Ensuite, ver +1, et enregistrez cet enregistrement. De cette façon, la mise à jour de l'enregistrement est terminée. Dans le même temps, le numéro de version de l'enregistrement est également augmenté de 1.

Étant donné que le processus de mise à jour d'ActiveRecord nécessite finalement d'appeler yiidbBaseActiveRecord::updateInteranl() , bien sûr, le code de gestion du verrouillage optimiste est caché dans cette méthode :


protected function updateInternal($attributes = null)
{
  if (!$this->beforeSave(false)) {
    return false;
  }
  // 获取等下要更新的字段及新的字段值
  $values = $this->getDirtyAttributes($attributes);
  if (empty($values)) {
    $this->afterSave(false, $values);
    return 0;
  }
  // 把原来ActiveRecord的主键作为等下更新记录的条件,
  // 也就是说,等下更新的,最多只有1个记录。
  $condition = $this->getOldPrimaryKey(true);

  // 获取版本号字段的字段名,比如 ver
  $lock = $this->optimisticLock();

  // 如果 optimisticLock() 返回的是 null,那么,不启用乐观锁。
  if ($lock !== null) {
    // 这里的 $this->$lock ,就是 $this->ver 的意思;
    // 这里把 ver+1 作为要更新的字段之一。
    $values[$lock] = $this->$lock + 1;

    // 这里把旧的版本号作为更新的另一个条件
    $condition[$lock] = $this->$lock;
  }
  $rows = $this->updateAll($values, $condition);

  // 如果已经启用了乐观锁,但是却没有完成更新,或者更新的记录数为0;
  // 那就说明是由于 ver 不匹配,记录被修改过了,于是抛出异常。
  if ($lock !== null && !$rows) {
    throw new StaleObjectException('The object being updated is outdated.');
  }
  $changedAttributes = [];
  foreach ($values as $name => $value) {
    $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
    $this->_oldAttributes[$name] = $value;
  }
  $this->afterSave(false, $changedAttributes);
  return $rows;
}

À partir du code ci-dessus, nous pouvons facilement conclure :

  1. Lorsque optimisticLock() renvoie null, le verrouillage optimiste ne sera pas activé.

  2. Le numéro de version ne fait qu'augmenter.

  3. Il y a deux conditions pour réussir le verrouillage optimiste. L'une est que la clé primaire doit exister et l'autre est que la mise à jour doit être terminée.

  4. Lorsque le verrouillage optimiste est activé, StaleObjectException sera levée uniquement dans les deux situations suivantes :

    1. Lorsque l'enregistrement est supprimé par autres Enfin, la mise à jour a échoué car la clé primaire n'existait plus.

    2. Le numéro de version a été modifié et la deuxième condition de mise à jour n'est pas remplie.

Processus de suppression

Par rapport au processus de mise à jour, le verrouillage optimiste du processus de suppression est plus simple et mieux compris. Le code est toujours dans yiidbBaseActiveRecord :


public function delete()
{
  $result = false;
  if ($this->beforeDelete()) {
    // 删除的SQL语句中,WHERE部分是主键
    $condition = $this->getOldPrimaryKey(true);
    // 获取版本号字段的字段名,比如 ver
    $lock = $this->optimisticLock();
    // 如果启用乐观锁,那么WHERE部分再加一个条件,版本号
    if ($lock !== null) {
      $condition[$lock] = $this->$lock;
    }
    $result = $this->deleteAll($condition);
    if ($lock !== null && !$result) {
      throw new StaleObjectException('The object being deleted is outdated.');
    }
    $this->_oldAttributes = null;
    $this->afterDelete();
  }
  return $result;
}

比起更新过程,删除过程确实要简单得多。唯一的区别就是省去了版本号+1的步骤。 都要删除了,版本号+1有什么意义?

乐观锁失效

乐观锁存在失效的情况,属小概率事件,需要多个条件共同配合才会出现。如:

  1. 应用采用自己的策略管理主键ID。如,常见的取当前ID字段的最大值+1作为新ID。

  2. 版本号字段 ver 默认值为 0 。

  3. 用户A读取了某个记录准备修改它。该记录正好是ID最大的记录,且之前没被修改过, ver 为默认值 0。

  4. 在用户A读取完成后,用户B恰好删除了该记录。之后,用户C又插入了一个新记录。

  5. 此时,阴差阳错的,新插入的记录的ID与用户A读取的记录的ID是一致的, 而版本号两者又都是默认值 0。

  6. 用户A在用户C操作完成后,修改完成记录并保存。由于ID、ver均可以匹配上, 因此用户A成功保存。但是,却把用户C插入的记录覆盖掉了。

乐观锁此时的失效,根本原因在于应用所使用的主键ID管理策略, 正好与乐观锁存在极小程度上的不兼容。

两者分开来看,都是没问题的。组合到一起之后,大致看去好像也没问题。 但是bug之所以成为bug,坑之所以能够坑死人,正是由于其隐蔽性。

对此,也有一些意见提出来,使用时间戳作为版本号字段,就可以避免这个问题。 但是,时间戳的话,如果精度不够,如毫秒级别,那么在高并发,或者非常凑巧情况下, 仍有失效的可能。而如果使用高精度时间戳的话,成本又太高。

使用时间戳,可靠性并不比使用整型好。问题还是要回到使用严谨的主键成生策略上来。

悲观锁

正如其名字,悲观锁(pessimistic locking)体现了一种谨慎的处事态度。其流程如下:

  1. 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。

  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。

  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

  4. 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

悲观锁确实很严谨,有效保证了数据的一致性,在C/S应用上有诸多成熟方案。 但是他的缺点与优点一样的明显:

  1. 悲观锁适用于可靠的持续性连接,诸如C/S应用。 对于Web应用的HTTP连接,先天不适用。

  2. 锁的使用意味着性能的损耗,在高并发、锁定持续时间长的情况下,尤其严重。 Web应用的性能瓶颈多在数据库处,使用悲观锁,进一步收紧了瓶颈。

  3. 非正常中止情况下的解锁机制,设计和实现起来很麻烦,成本还很高。

  4. 不够严谨的设计下,可能产生莫名其妙的,不易被发现的, 让人头疼到想把键盘一巴掌碎的死锁问题。

总体来看,悲观锁不大适应于Web应用,Yii团队也认为悲观锁的实现过于麻烦, 因此,ActiveRecord也没有提供悲观锁。

作为Yii的构成基因之一的Ruby on rails,他的ActiveReocrd模型,倒是提供了悲观锁, 但是使用起来也很麻烦。

悲观锁的实现

虽然悲观锁在Web应用上存在诸多不足,实现悲观锁也需要解决各种麻烦。但是, 当用户提出他就是要用悲观锁时,牙口再不好的码农,就是咬碎牙也是要啃下这块骨头来。

对于一个典型的Web应用而言,这里提供个人常用的方法来实现悲观锁。

首先,在要锁定的表里,加一个字段如 locked_at ,表示当前记录被锁定时的时间, 当为 0 时,表示该记录未被锁定,或者认为这是1970年时加的锁。

当要修改某个记录时,先看看当前时间与 locked_at 字段相差是否超过预定的一个时长T,比如 30 min ,1 h 之类的。

如果没超过,说明该记录有人正在修改,我们暂时不能打开(读取)他来修改。 否则,说明可以修改,我们先将当前时间戳保存到该记录的 locked_at 字段。 那么之后的时长T内如果有人要来改这个记录,他会由于加锁失败而无法读取, 从而无法修改。

我们在完成修改后,即将保存时,要比对现在的 locked_at 。只有在 locked_at 一致时,才认为刚刚是我们加的锁,我们才可以保存。 否则,说明在我们加锁后,又有人加了锁正在修改, 或者已经完成了修改,使得 locked_at 归 0。

这种情况主要是由于我们的修改时长过长,超过了预定的T。原先的加锁自动解开, 其他用户可以在我们加锁时刻再过T之后,重新加上自己的锁。换句话说, 此时悲观锁退化为乐观锁。

大致的原理性代码如下:


// 悲观锁AR基类,需要使用悲观锁的AR可以由此派生
class PLockAR extends \yii\db\BaseActiveRecord {
  // 声明悲观锁使用的标记字段,作用类似于 optimisticLock() 方法
  public function pesstimisticLock() {
    return null;
  }

  // 定义锁定的最大时长,超过该时长后,自动解锁。
  public function maxLockTime() {
    return 0;
  }

  // 尝试加锁,加锁成功则返回true
  public function lock() {
    $lock = $this->pesstimisticLock();
    $now = time();
    $values = [$lock => $now];
    // 以下2句,更新条件为主键,且上次锁定时间距现在超过规定时长
    $condition = $this->getOldPrimaryKey(true);
    $condition[] = [&#39;<&#39;, $lock, $now - $this->maxLockTime()];

    $rows = $this->updateAll($values, $condition);
    // 加锁失败,返回 false
    if (! $rows) {
      return false;
    }
    return true;
  }

  // 重载updateInternal()
  protected function updateInternal($attributes = null)
  {
    // 这些与原来代码一样
    if (!$this->beforeSave(false)) {
      return false;
    }
    $values = $this->getDirtyAttributes($attributes);
    if (empty($values)) {
      $this->afterSave(false, $values);
      return 0;
    }
    $condition = $this->getOldPrimaryKey(true);

    // 改为获取悲观锁标识字段
    $lock = $this->pesstimisticLock();

    // 如果 $lock 为 null,那么,不启用悲观锁。
    if ($lock !== null) {
      // 等下保存时,要把标识字段置0
      $values[$lock] = 0;

      // 这里把原来的标识字段值作为更新的另一个条件
      $condition[$lock] = $this->$lock;
    }
    $rows = $this->updateAll($values, $condition);

    // 如果已经启用了悲观锁,但是却没有完成更新,或者更新的记录数为0;
    // 那就说明之前的加锁已经自动失效了,记录正在被修改,
    // 或者已经完成修改,于是抛出异常。
    if ($lock !== null && !$rows) {
      throw new StaleObjectException(&#39;The object being updated is outdated.&#39;);
    }
    $changedAttributes = [];
    foreach ($values as $name => $value) {
      $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
      $this->_oldAttributes[$name] = $value;
    }
    $this->afterSave(false, $changedAttributes);
    return $rows;
  }
}

上面的代码对比乐观锁,主要不同点在于:

  1. 新增加了一个加锁方法,一个获取锁定最大时长的方法。

  2. 保存时不再是把标识字段+1,而是把标识字段置0。

在具体使用方法上,可以参照以下代码:


// 从PLockAR派生模型类
class Post extends PLockAR {
  // 重载定义悲观锁标识字段,如 locked_at
  public function pesstimisticLock() {
    return &#39;locked_at&#39;;
  }
  // 重载定义最大锁定时长,如1小时
  public function maxLockTime() {
    return 3600000;
  }
}

// 修改前要尝试加锁
class SectionController extends Controller {
  public function actionUpdate($id)
  {
    $model = $this->findModel($id);

    if ($model->load(Yii::$app->request->post()) && $model->save()) {
      return $this->redirect([&#39;view&#39;, &#39;id&#39; => $model->id]);
    } else {
      // 加入一个加锁的判断
      if (!$model->lock()) {
        // 加锁失败
        // ... ...
      }
      return $this->render(&#39;update&#39;, [
        &#39;model&#39; => $model,
      ]);
    }
  }
}

上述方法实现的悲观锁,避免了使用数据库自身的锁机制,契合Web应用的特点, 具有一定的适用性,但是也存在一定的缺陷:

  1. 最长允许锁定时长会带来一定的副作用。时间定得长了,可能要等很长时间, 才能重新编辑非正常解锁的记录。时间定得短了,则经常退化成乐观锁。

  2. 时间戳精度问题。如果精度不够,那么在加锁时,与我们讨论过的乐观锁失效存, 在同样的漏洞。

  3. 这种形式的锁定,只是应用层面的锁定,并非数据库层面的锁定。 如果存在应用之外对于数据库的写入操作。这个锁定机制是无效的。

相关推荐:

实现redis中事务机制及乐观锁的方法

MySQL数据库优化(三)—MySQL悲观锁和乐观锁(并发控制)

悲观锁和乐观锁的比较和使用

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn