Heim  >  Artikel  >  Backend-Entwicklung  >  Ausführliche Erläuterung der Beispiele für optimistisches und pessimistisches Sperren in Yii2.0

Ausführliche Erläuterung der Beispiele für optimistisches und pessimistisches Sperren in Yii2.0

小云云
小云云Original
2018-02-01 11:14:192328Durchsuche

Webanwendungen sind oft mit Umgebungen mit mehreren Benutzern konfrontiert. In diesem Fall ist die gleichzeitige Schreibkontrolle zu einer Fähigkeit geworden, die fast jeder Entwickler beherrschen muss. Dieser Artikel stellt hauptsächlich die Prinzipien und Verwendungsmöglichkeiten des optimistischen und pessimistischen Sperrens in Yii2.0 vor. Ich hoffe, er kann Ihnen helfen.

In einer gleichzeitigen Umgebung können Situationen wie Dirty Read, Unrepeatable Read, Phantom Read, Lost Update usw. auftreten. Sie können selbst nach einer bestimmten Leistung suchen.

Um diese Probleme zu lösen, bieten Mainstream-Datenbanken Sperrmechanismen und führen das Konzept der Transaktionsisolationsstufe ein. Wir werden es hier nicht erklären. Suchen Sie einfach nach diesen Schlüsselwörtern und Sie werden viele davon im Internet finden.

Was den spezifischen Entwicklungsprozess betrifft, gibt es jedoch im Allgemeinen zwei Methoden, um das Problem von Parallelitätskonflikten zu lösen: pessimistisches Sperren und optimistisches Sperren.

Optimistisches Sperren

Optimistisches Sperren zeigt eine mutige und pragmatische Haltung. Die Voraussetzung für die Verwendung optimistischer Sperren besteht darin, dass in tatsächlichen Anwendungen die Konfliktwahrscheinlichkeit relativ gering ist. Sein Entwurf und seine Umsetzung sind direkt und prägnant. In aktuellen Webanwendungen ist die Verwendung von optimistischem Sperren ein absoluter Vorteil.

Daher bietet Yii auch optimistische Sperrunterstützung für ActiveReocrd.

Laut der offiziellen Dokumentation von Yii ist die Verwendung der optimistischen Sperre in vier Schritte unterteilt:

  • Fügen Sie der Tabelle ein Feld hinzu, das gesperrt werden muss, um die Versionsnummer darzustellen. Natürlich muss auch das entsprechende Modell dem Feld hinzugefügt und entsprechende Anpassungen vorgenommen werden. Dieses Feld sollte beispielsweise zu Rules() hinzugefügt werden.

  • Überladen Sie die Methode yiidbActiveRecord::optimisticLock(), um den Feldnamen im vorherigen Schritt zurückzugeben.

  • Fügen Sie im Formular der Datensatzänderungsseite ein 10a0c4c6fe334f1dd2642c4aa224944a hinzu, um die Versionsnummer des Datensatzes beim Lesen vorübergehend zu speichern.

  • Wenn Sie den Code speichern, verwenden Sie try ... Catch, um zu sehen, ob Sie eine yiidbStaleObjectException-Ausnahme abfangen können. Wenn ja, bedeutet dies, dass der Datensatz während dieser Änderung des Datensatzes geändert wurde. Wenn es sich um eine einfache Antwort handelt, können Sie entsprechende Eingabeaufforderungen geben. Wenn Sie schlau sind, können Sie nicht widersprüchliche Änderungen zusammenführen oder eine Diff-Seite anzeigen.

Im Wesentlichen verwendet das optimistische Sperren nicht den Datenbanksperrmechanismus wie das pessimistische Sperren. Beim optimistischen Sperren wird der Tabelle ein Zählfeld hinzugefügt, das angibt, wie oft der aktuelle Datensatz geändert wurde (Versionsnummer).

Implementieren Sie dann eine optimistische Sperrung, indem Sie die Versionsnummern vor dem Aktualisieren oder Löschen vergleichen.

Deklarieren Sie das Versionsnummernfeld

Die Versionsnummer ist die Grundlage für die Implementierung optimistischer Sperren. Der erste Schritt besteht also darin, Yii mitzuteilen, welches Feld das Versionsnummernfeld ist. Dies wird von yiidbBaseActiveRecord erledigt:


public function optimisticLock()
{
  return null;
}

Diese Methode gibt null zurück, was darauf hinweist, dass keine optimistische Sperre verwendet wird. Dann müssen wir dies in unserem Modell überladen. Gibt eine Zeichenfolge zurück, die das Feld darstellt, das wir zur Identifizierung der Versionsnummer verwendet haben. Es kann beispielsweise so aussehen:


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

Dies bedeutet, dass im aktuellen ActiveRecord ein Ver-Feld vorhanden ist, das für eine optimistische Sperre verwendet werden kann. Wie nutzt Yii dieses Ver-Feld, um eine optimistische Sperre zu implementieren?

Aktualisierungsprozess

Konkret handelt es sich bei dem Aktualisierungsprozess nach Verwendung der optimistischen Sperre um einen solchen Prozess:

  1. Lesen Sie den Datensatz aktualisiert werden.

  2. Ändern Sie den Datensatz entsprechend den Wünschen des Benutzers. Natürlich wird das ver-Feld zu diesem Zeitpunkt nicht geändert. Dieses Feld ist für den Benutzer bedeutungslos.

  3. Bevor Sie den Datensatz speichern, lesen Sie das Ver-Feld des Datensatzes erneut aus und vergleichen Sie ihn mit dem zuvor gelesenen Wert.

  4. Wenn ver unterschiedlich ist, bedeutet dies, dass dieser Datensatz während des Benutzeränderungsprozesses von anderen geändert wurde. Deshalb geben wir Ihnen einen Hinweis.

  5. Wenn die Version gleich ist, bedeutet dies, dass dieser Datensatz nicht geändert wurde. Dann ver +1 und speichern Sie diesen Datensatz. Auf diese Weise ist die Datensatzaktualisierung abgeschlossen. Gleichzeitig wird auch die Versionsnummer des Datensatzes um 1 erhöht.

Da der Aktualisierungsprozess von ActiveRecord letztendlich den Aufruf von yiidbBaseActiveRecord::updateInteranl() erfordert, ist der Code für die Behandlung optimistischer Sperren natürlich in dieser Methode verborgen:


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;
}

Aus dem obigen Code können wir leicht schließen:

  1. Wenn optimisticLock() null zurückgibt, wird die optimistische Sperre nicht aktiviert.

  2. Die Versionsnummer erhöht sich nur.

  3. Es gibt zwei Bedingungen für das Bestehen der optimistischen Sperre: Zum einen muss der Primärschlüssel vorhanden sein, zum anderen muss die Aktualisierung abgeschlossen sein.

  4. Wenn die optimistische Sperre aktiviert ist, wird StaleObjectException nur in den folgenden zwei Situationen ausgelöst:

    1. Wenn der Datensatz gelöscht wird Andere Schließlich scheiterte das Update, weil der Primärschlüssel nicht mehr existierte.

    2. Die Versionsnummer wurde geändert und die zweite Bedingung für die Aktualisierung ist nicht erfüllt.

Löschvorgang

Im Vergleich zum Aktualisierungsprozess ist die optimistische Sperrung des Löschvorgangs einfacher und besser verständlich. Der Code befindet sich noch in 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悲观锁和乐观锁(并发控制)

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

Das obige ist der detaillierte Inhalt vonAusführliche Erläuterung der Beispiele für optimistisches und pessimistisches Sperren in Yii2.0. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn