Web applications tend to face multi-user environments, where concurrent write control is almost a skill that every developer must master. This paper mainly introduces the principle and use of Yii2.0 optimistic lock and pessimistic lock, hoping to help everyone.
In a concurrent environment, it is possible to have dirty reads (Dirty read), non-repeatable reads (unrepeatable read), Phantom reads (Phantom read), Lost Updates (Lost update), and so on. Specific performance can be self-search.
To address these issues, the mainstream database provides a lock mechanism and introduces the concept of transaction isolation levels. Here we do not explain, take these keywords a search, online a lot of.
However, in the specific development process, generally divided into pessimistic lock and optimistic locking two ways to solve the problem of concurrency conflicts.
Optimistic lock
The optimistic lock (optimistic locking) shows a bold and pragmatic attitude. The premise of using optimistic locking is that in practice, the probability of conflict is relatively low. His designs and implementations are straightforward and concise. In the current Web application, the use of optimistic locks occupies an absolute advantage.
As a result, Yii also provides optimistic lock support for ACTIVEREOCRD.
According to Yii's official documentation, the use of optimistic locks, a total of 4 steps:
Adds a field to the table that needs to be locked to represent the version number. Of course, the corresponding model should be added to the field to make appropriate adjustments. For example, the field is added to the rules ().
Overloads the Yii\db\activerecord::optimisticlock () method to return the field name from the previous step.
In the modified page form of the record, add a <input type= "hidden" > The version number of the record for the staging read.
In the place where you saved your code, use the try ... catch to see if you can catch a Yii\db\staleobjectexception exception. If so, the record has been modified in the process of modifying this record. If you have a simple response, you can make the appropriate prompt. Smart points, you can make changes that are not conflicting, or display a diff page.
In essence, optimistic locking does not use the lock mechanism of a database like a pessimistic lock. Optimistic locking indicates the number of times the current record has been modified (version number) by adding a Count field to the table.
The optimistic lock is then implemented by the version number before updating and deleting.
Declaring the version number field
The version number is the root of the optimistic lock implementation. So the first step, we're going to tell Yii, which field is the version number field. This is the responsibility of Yii\db\baseactiverecord:
Public Function Optimisticlock () { return null;}
This method returns null, which means that optimistic locks are not used. So in our model, we're going to overload this. Returns a String that represents the field that we use to identify the version number. For example, this can be:
Public Function Optimisticlock () { return ' ver ';}
Description of the current ActiveRecord, there is a ver field that can be used for optimistic locking. So how does yii specifically use this ver field to achieve optimistic locking?
Update process
Specifically, the update process after using optimistic locking is such a process:
Read the record that you want to update.
Make changes to the records according to the user's wishes. Of course, the Ver field is not modified at this time. This field is meaningless to the user.
Before you save the record, read the Ver field of the record again, and compare it to the previously read value.
If the ver is different, it means that the record was changed by someone else during the user's modification. Well, we're going to give a hint.
If the ver is the same, the record has not been modified. Well, on Ver +1, and save this record. This completes the update of the record. At the same time, the version number of the record is also added by 1.
Since the ActiveRecord update process ultimately calls for a call, it is yii\db\BaseActiveRecord::updateInteranl()
natural that the code that handles optimistic locking is hidden in this method:
protected function updateinternal ($attributes = null) {if (! $this->beforesave (False)) {return false; }//Get the fields to be updated and the new field values $values = $this->getdirtyattributes ($attributes); if (empty ($values)) {$this->aftersave (false, $values); return 0; }//The original ActiveRecord primary key as the condition of the update record,//That is, wait for the update, up to only 1 records. $condition = $this->getoldprimarykey (true); Gets the field name of the version number field, such as ver $lock = $this->optimisticlock (); If Optimisticlock () returns NULL, then optimistic locking is not enabled. if ($lock!== null) {//The $this here, $lock, is the meaning of $this->ver;//This place ver+1 as one of the fields to be updated. $values [$lock] = $this, $lock + 1; Here the old version number as another condition of the update $condition [$lock] = $this-$lock; } $rows = $this->updateall ($values, $condition); If optimistic locking is enabled, but the update is not completed, or the number of records updated is 0,//That is because Ver does not match, the record has been modified, and throws an exception. 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;}
From the above code, it is not difficult to draw:
Optimistic locks are not enabled when Optimisticlock () returns NULL.
The version number only increases.
There are 2 conditions through optimistic locking, one is the primary key to exist, and the other is to be able to complete the update.
When optimistic locking is enabled, only the following two cases throw staleobjectexception exceptions:
When the record is deleted by someone else, the update fails because the primary key no longer exists.
The version number has changed and does not meet the second condition of the update.
Delete procedure
The optimistic lock of the deletion process is simpler and better understood than the update process. The code is still in Yii\db\baseactiverecord:
Public Function Delete () { $result = false; if ($this->beforedelete ()) { //delete the SQL statement, the where part is the primary key $condition = $this->getoldprimarykey (true); Gets the field name of the version number field, such as ver $lock = $this->optimisticlock (); If optimistic locking is enabled, then the where section adds a condition, the version number 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;}
The removal process is really much simpler than the update process. The only difference is that the step of version number +1 is omitted. Have to be removed, what is the meaning of version number +1?
Optimistic lock failure
Optimistic lock is a case of failure, which is a small probability event, which requires a combination of multiple conditions to be present. Such as:
The app uses its own policy to manage the primary key ID. For example, the most common fetch is the maximum value of the current ID field of +1 as the new ID.
Version Number field ver The default value is 0.
User A reads a record and prepares to modify it. This record is exactly the record with the highest ID and has not been modified before, and Ver is the default value of 0.
When user A reads is complete, User B deletes the record exactly. Then, User C inserts a new record.
At this point, quirks, the ID of the newly inserted record is consistent with the ID of the record read by user A, and the version number is both the default value of 0.
User A changes the completion record and saves it after the user C operation is complete. User A is saved successfully because the ID and ver can match. However, the user C inserted record is overwritten.
The reason for the failure of the optimistic lock at this time is that the primary key ID management policy used by the application is very small incompatible with the optimistic lock.
Apart from the two, it's all fine. After the combination, it seems to be okay to look at it roughly. But the bug became a bug, the pit is able to pit the dead, precisely because of its concealment.
For this, there are some suggestions that using timestamps as the version number field, you can avoid this problem. However, timestamps, if the accuracy is not enough, such as the millisecond level, then in high concurrency, or very unfortunate situation, there is still the possibility of failure. And if you use a high-precision timestamp, the cost is too high.
With timestamps, reliability is no better than using an integral type. The problem is to go back to using a rigorous primary key genetic strategy.
Pessimistic lock
As its name, pessimistic lock (pessimistic locking) embodies a cautious attitude. The process is as follows:
Try to add an exclusive lock (exclusive locking) to the record before making any changes to it.
If the lock fails, indicating that the record is being modified, the current query may have to wait or throw an exception. The specific response is determined by the developer according to the actual needs.
If the lock is successful, the record can be modified and the transaction will be unlocked when it is completed.
In the meantime, if there are other actions to modify or add exclusive locks to the record, it will wait for us to unlock or throw the exception directly.
Pessimistic lock is really rigorous, effectively ensure the consistency of data, in C/S application has many mature programs. But his shortcomings are as obvious as the merits:
Pessimistic locks are suitable for reliable, continuous connections such as C/s applications. For Web apps, the HTTP connection is not intended to be a priori.
The use of locks means the loss of performance, especially in cases of high concurrency and long lockout duration. The performance bottleneck of Web application is more in the database, using pessimistic lock, further tightening the bottleneck.
The unlocking mechanism in the case of abnormal abort is cumbersome to design and implement, and the cost is very high.
Not rigorous design, may produce inexplicable, not easy to be found, let a person headache to want to put the keyboard a slap broken deadlock problem.
Overall, pessimistic lock is not suitable for Web application, Yii team also think pessimistic lock implementation is too troublesome, therefore, ActiveRecord also did not provide pessimistic lock.
As one of Yii's constituent genes, Ruby on Rails, his ACTIVEREOCRD model, offers a pessimistic lock, but is also cumbersome to use.
Implementation of pessimistic lock
Although pessimistic lock in the Web application there are many shortcomings, to achieve pessimistic lock also need to solve various problems. However, when the user proposed that he is to use pessimistic lock, teeth again bad yard, is to bite the tooth is also to chew down the bone.
For a typical Web application, this provides a personal common way to implement pessimistic locking.
First, in the table to be locked, add a field such as Locked_at, indicating the time when the current record is locked, when it is 0 o'clock, indicates that the record is not locked, or that it is a lock that was added in 1970.
When you want to modify a record, let's see if the current time differs from the Locked_at field by a predetermined length of t, such as 1 min.
If it is not exceeded, it indicates that the record is being modified and we cannot open (read) it for the moment. Otherwise, the description can be modified, and we will first save the current timestamp to the Locked_at field of the record. After that, if someone is going to change the record, he will not be able to read it because of the lock failure and cannot be modified.
After we have finished the changes, we are going to save the locked_at than the present. It is only when the locked_at is consistent that we are able to preserve the lock we have just added. Otherwise, after we lock, someone added a lock is being modified, or has completed the modification, so that locked_at 0.
This is mainly due to the fact that our modifications are too long to exceed the predetermined T. The original lock is automatically unlocked, and the other user can add their own lock after we lock the moment and then after T. In other words, pessimistic locks degenerate into optimistic locks at this time.
The approximate principle code is as follows:
Pessimistic lock AR base class, need to use pessimistic lock ar can derive from this class Plockar extends \yii\db\baseactiverecord {//Declare pessimistic lock using the tag field, acting like the Optimisticlock () method Pu Blic function Pesstimisticlock () {return null; }//Defines the maximum duration of the lock, which is automatically unlocked after the length of time has passed. Public Function Maxlocktime () {return 0; }//try to lock, lock successfully returns true public Function lock () {$lock = $this->pesstimisticlock (); $now = time (); $values = [$lock + $now]; The following 2 sentences update the condition as the primary key, and the last lock time span now exceeds the specified length $condition = $this->getoldprimarykey (true); $condition [] = [' < ', $lock, $now-$this->maxlocktime ()]; $rows = $this->updateall ($values, $condition); Lock failed, return FALSE if (! $rows) {return false; } return true; }//Overload updateinternal () protected function updateinternal ($attributes = null) {//These are the same as the original code if (! $this->befor Esave (False)) {return false; } $values = $this->getdirtyattributes ($attributes); if (empty ($values)) {$this->aftersave (false, $values); return 0; } $condition = $thIs->getoldprimarykey (TRUE); Instead, get the pessimistic lock identification field $lock = $this->pesstimisticlock (); If $lock is null, pessimistic locks are not enabled. if ($lock!== null) {//wait for Save, place the identity field 0 $values [$lock] = 0; Here the original Identity field value as another condition of the update $condition [$lock] = $this, $lock; } $rows = $this->updateall ($values, $condition); If the pessimistic lock has been enabled, but the update is not completed, or the number of records updated is 0;//That means that the previous locking has been automatically invalidated, the record is being modified,//or the modification has been completed, and throws an exception. 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; }}
The above code compares the optimistic lock, the main difference is:
A new lock method is added, a method to get the maximum length of the lock.
Save is no longer the identity field +1, but the identity field is set to 0.
For specific use, you can refer to the following code:
Derived from Plockar model classes class Post extends Plockar { //overloads define pessimistic lock identification fields, such as Locked_at public function Pesstimisticlock () { return ' Locked_at '; } Overload defines the maximum lock duration, such as 1 -hour public Function maxlocktime () { return 3600000; }} To try to lock class Sectioncontroller extends Controller {public function actionupdate ($id) { $model = $) before modifying This->findmodel ($id); if ($model->load (Yii:: $app->request->post ()) && $model->save ()) { return $this Redirect ([' View ', ' id ' = + $model->id]); } else { //Add a lock to determine if (! $model->lock ()) { //Locking failed //... } return $this->render (' Update ', [ ' model ' = ' $model, ]} } }
The pessimistic lock implemented by the above method avoids the use of the database's own lock mechanism, which is fit for the characteristics of Web application, and has certain applicability, but it also has some defects:
The maximum allowable lock length can cause some side effects. The time is long, and it may take a long time before you can re-edit the record that is not normally unlocked. When the time is set short, it often degenerates into an optimistic lock.
Time stamp accuracy issue. If the accuracy is not enough, then in the lock, and we discussed the optimistic lock failure, in the same loophole.
This form of locking is only application-level locking, not database-level locking. If there is a write operation for the database outside of the app. This locking mechanism is not valid.