Home Backend Development PHP Tutorial Detailed explanation of optimistic locking and pessimistic locking examples in Yii2.0

Detailed explanation of optimistic locking and pessimistic locking examples in Yii2.0

Feb 01, 2018 am 11:14 AM
Example Detailed explanation

Web applications often face multi-user environments. Concurrent write control in this case has become a skill that almost every developer must master. This article mainly introduces the principles and uses of optimistic locking and pessimistic locking in Yii2.0. I hope it can help you.

In a concurrent environment, Dirty Read, Unrepeatable Read, Phantom Read, Lost update, etc. may occur. You can search for specific performance by yourself.

In order to deal with these problems, mainstream databases provide lock mechanisms and introduce the concept of transaction isolation level. We won’t explain it here. Just search for these keywords and you’ll find a lot of them on the Internet.

However, as far as the specific development process is concerned, there are generally two methods to solve the problem of concurrency conflicts: pessimistic locking and optimistic locking.

Optimistic Lock

Optimistic locking (optimistic locking) shows a bold and pragmatic attitude. The premise of using optimistic locking is that in actual applications, the probability of conflict is relatively low. His design and implementation are direct and concise. In current web applications, the use of optimistic locking has an absolute advantage.

Therefore, Yii also provides optimistic locking support for ActiveReocrd.

According to Yii's official documentation, using optimistic locking is divided into 4 steps:

  • Add a field to the table that needs to be locked to represent the version number. Of course, the corresponding Model must also be added to the field and make appropriate adjustments. For example, this field should be added to rules().

  • Overload the yii\db\ActiveRecord::optimisticLock() method to return the field name in the previous step.

  • In the record modification page form, add an to temporarily store the version number of the record when reading.

  • Where you save the code, use try ... catch to see if you can catch a yii\db\StaleObjectException exception. If so, it means that the record has been modified during this modification of the record. If it is a simple response, you can give corresponding prompts. If you are smart, you can merge non-conflicting changes or display a diff page.

Essentially speaking, optimistic locking does not use the database locking mechanism like pessimistic locking. Optimistic locking adds a count field to the table to represent the number of times the current record has been modified (version number).

Then implement optimistic locking by comparing version numbers before updating or deleting.

Declare the version number field

The version number is the foundation for implementing optimistic locking. So the first step is to tell Yii which field is the version number field. This is handled by yii\db\BaseActiveRecord:


public function optimisticLock()
{
  return null;
}
Copy after login

This method returns null, indicating that optimistic locking is not used. Then we need to overload this in our Model. Returns a string representing the field we used to identify the version number. For example, it can be like this:


public function optimisticLock()
{
  return 'ver';
}
Copy after login

It means that there is a ver field in the current ActiveRecord, which can be used for optimistic locking. So how does Yii use this ver field to implement optimistic locking?

Update process

Specifically speaking, the update process after using optimistic locking is such a process:

  1. Read Get the record to be updated.

  2. Modify the record according to the user's wishes. Of course, the ver field will not be modified at this time. This field is meaningless to the user.

  3. Before saving the record, read the ver field of the record again and compare it with the previously read value.

  4. If ver is different, it means that this record has been modified by others during the user modification process. So, we're going to give you a hint.

  5. If ver is the same, it means that this record has not been modified. Then, ver +1, and save this record. In this way, the record update is completed. At the same time, the version number of the record is also increased by 1.

Since the update process of ActiveRecord ultimately requires calling yii\db\BaseActiveRecord::updateInteranl() , of course, the code that handles optimistic locking is hidden. In this method:


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;
}
Copy after login

From the above code, we can easily figure out:

  1. When optimisticLock( ) returns null, optimistic locking will not be enabled.

  2. The version number only increases, not decreases.

  3. There are two conditions for passing optimistic locking. One is that the primary key must exist, and the other is that the update must be completed.

  4. When optimistic locking is enabled, StaleObjectException will be thrown in only the following two situations:

    1. When the record is deleted by others Finally, the update failed because the primary key no longer existed.

    2. The version number has been changed and the second condition for update is not met.

Deletion process

Compared with the update process, the optimistic locking of the deletion process is simpler and easier to understand. The code is still in yii\db\BaseActiveRecord:


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;
}
Copy after login

比起更新过程,删除过程确实要简单得多。唯一的区别就是省去了版本号+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;
  }
}
Copy after login

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

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

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

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

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

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

相关推荐:

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

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

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

The above is the detailed content of Detailed explanation of optimistic locking and pessimistic locking examples in Yii2.0. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Detailed explanation of obtaining administrator rights in Win11 Detailed explanation of obtaining administrator rights in Win11 Mar 08, 2024 pm 03:06 PM

Windows operating system is one of the most popular operating systems in the world, and its new version Win11 has attracted much attention. In the Win11 system, obtaining administrator rights is an important operation. Administrator rights allow users to perform more operations and settings on the system. This article will introduce in detail how to obtain administrator permissions in Win11 system and how to effectively manage permissions. In the Win11 system, administrator rights are divided into two types: local administrator and domain administrator. A local administrator has full administrative rights to the local computer

Detailed explanation of division operation in Oracle SQL Detailed explanation of division operation in Oracle SQL Mar 10, 2024 am 09:51 AM

Detailed explanation of division operation in OracleSQL In OracleSQL, division operation is a common and important mathematical operation, used to calculate the result of dividing two numbers. Division is often used in database queries, so understanding the division operation and its usage in OracleSQL is one of the essential skills for database developers. This article will discuss the relevant knowledge of division operations in OracleSQL in detail and provide specific code examples for readers' reference. 1. Division operation in OracleSQL

Detailed explanation of the role and usage of PHP modulo operator Detailed explanation of the role and usage of PHP modulo operator Mar 19, 2024 pm 04:33 PM

The modulo operator (%) in PHP is used to obtain the remainder of the division of two numbers. In this article, we will discuss the role and usage of the modulo operator in detail, and provide specific code examples to help readers better understand. 1. The role of the modulo operator In mathematics, when we divide an integer by another integer, we get a quotient and a remainder. For example, when we divide 10 by 3, the quotient is 3 and the remainder is 1. The modulo operator is used to obtain this remainder. 2. Usage of the modulo operator In PHP, use the % symbol to represent the modulus

Detailed explanation of the linux system call system() function Detailed explanation of the linux system call system() function Feb 22, 2024 pm 08:21 PM

Detailed explanation of Linux system call system() function System call is a very important part of the Linux operating system. It provides a way to interact with the system kernel. Among them, the system() function is one of the commonly used system call functions. This article will introduce the use of the system() function in detail and provide corresponding code examples. Basic Concepts of System Calls System calls are a way for user programs to interact with the operating system kernel. User programs request the operating system by calling system call functions

Detailed explanation of Linux curl command Detailed explanation of Linux curl command Feb 21, 2024 pm 10:33 PM

Detailed explanation of Linux's curl command Summary: curl is a powerful command line tool used for data communication with the server. This article will introduce the basic usage of the curl command and provide actual code examples to help readers better understand and apply the command. 1. What is curl? curl is a command line tool used to send and receive various network requests. It supports multiple protocols, such as HTTP, FTP, TELNET, etc., and provides rich functions, such as file upload, file download, data transmission, proxy

Learn more about Promise.resolve() Learn more about Promise.resolve() Feb 18, 2024 pm 07:13 PM

Detailed explanation of Promise.resolve() requires specific code examples. Promise is a mechanism in JavaScript for handling asynchronous operations. In actual development, it is often necessary to handle some asynchronous tasks that need to be executed in sequence, and the Promise.resolve() method is used to return a Promise object that has been fulfilled. Promise.resolve() is a static method of the Promise class, which accepts a

Learn best practice examples of pointer conversion in Golang Learn best practice examples of pointer conversion in Golang Feb 24, 2024 pm 03:51 PM

Golang is a powerful and efficient programming language that can be used to develop various applications and services. In Golang, pointers are a very important concept, which can help us operate data more flexibly and efficiently. Pointer conversion refers to the process of pointer operations between different types. This article will use specific examples to learn the best practices of pointer conversion in Golang. 1. Basic concepts In Golang, each variable has an address, and the address is the location of the variable in memory.

Starting from Scratch: Detailed Installation and Setup of VNC on Ubuntu Starting from Scratch: Detailed Installation and Setup of VNC on Ubuntu Dec 29, 2023 pm 04:27 PM

Starting from scratch: Detailed explanation of the installation and settings of VNC on Ubuntu On the Ubuntu operating system, VNC (Virtual Network Computing) is a remote desktop protocol that enables remote access and control of the Ubuntu desktop through a network connection. This article will detail the steps to install and set up VNC on Ubuntu, including specific code examples. Step 1: Install the VNC server. Open the terminal and enter the following command to update the software source and install the VNC server: sud

See all articles