Database concurrency control-lock and MVCC

After learning programming for a few years, you will find that there are no simple and quick solutions to all the problems. Many problems need to be weighed and compromised. This article introduces the trade-offs and trade-offs between concurrent performance and serialization of databases. Compromise-Concurrency control mechanism.

tradeoff-between-performance-and-serializability

If all transactions in the database are executed serially, it is very easy to become the performance bottleneck of the entire application. Although nodes that cannot be scaled horizontally will become the bottleneck in the end, a database that executes transactions serially will speed up this process. ; Concurrency makes it possible for everything to happen, it can solve certain performance problems, but it will bring more weird errors.

After the introduction of concurrent transactions, if you don't control the execution of the transaction, various problems will occur. You may have been dying by various strange problems without enjoying the performance improvement brought by concurrency.

Overview

How to control concurrency is one of the most important issues in the database field, but so far there have been many mature solutions for the control of transaction concurrency, and the principles of these solutions are what this article wants to introduce, which will be introduced in the article The three most common concurrency control mechanisms:

pessimistic-optimistic-multiversion-conccurency-control

They are pessimistic concurrency control, optimistic concurrency control, and multi-version concurrency control. Pessimistic concurrency control is actually the most common concurrency control mechanism, which is lock; and optimistic concurrency control actually has another name: optimistic locking, which is not actually A real lock, we will introduce it in detail in the later part of the article; the last is the multi-version concurrency control (MVCC), which is different from the opposite naming of the first two, MVCC can be used with either mechanism of the first two Used in combination to improve the read performance of the database.

Since this article introduces different concurrency control mechanisms, it will definitely involve the concurrency of different transactions. We will analyze how various mechanisms work through a schematic diagram.

Pessimistic concurrency control

Controlling the acquisition of the same data by different transactions is the most fundamental way to ensure the consistency of the database. If we can make transactions have the exclusive ability to monopolize the same resource at the same time, then we can ensure that different transactions operating on the same resource will not interact with each other. influences.

pessimistic-conccurency-control

The simplest and most widely used method is to use locks to solve the problem. When a transaction needs to operate on a resource, it needs to obtain the lock corresponding to the resource first. After ensuring that other transactions will not access the resource, perform various operations on the resource; In pessimistic concurrency control, the database program takes a pessimistic attitude towards data modification and will be locked in the process of data processing to solve the problem of competition.

Read-write lock

In order to maximize the concurrency of database transactions, the locks in the database are designed into two modes, namely shared locks and mutual exclusion locks. When a transaction obtains a shared lock, it can only perform read operations, so the shared lock is also called a read lock; when a transaction obtains a mutex lock for a row of data, it can read and write the row of data, so mutual Exclusive locks are also called write locks.

Shared-Exclusive-Lock

In addition to restricting the read and write operations that the transaction can perform, shared locks and mutex locks also have a "sharing" and "mutual exclusion" relationship, that is, multiple transactions can obtain a shared lock of a row of data at the same time, but Mutex locks are not compatible with shared locks and other mutexes. We can naturally understand the reason for this design: multiple transactions writing the same data at the same time will inevitably cause various weird problems.

lock-and-wait

If the current transaction has no way to acquire the lock corresponding to the row of data, it will fall into a waiting state, until other transactions release the lock corresponding to the current data to obtain the lock and perform the corresponding operation.

Two-phase lock protocol

The Two-Phase Locking Protocol (2PL) is a protocol that can guarantee the serialization of transactions. It divides the acquisition and release of locks into two different phases: Growing and Shrinking.

growing-to-shrinking

In the growth phase, a transaction can obtain locks but cannot release locks; while in the reduction phase, transactions can only release locks, but cannot obtain new locks. If you only look at the definition of 2PL, then it has been introduced here, but it still has Two variants:

  1. Strict 2PL : The mutex held by the transaction must be released after submission;
  2. Rigorous 2PL : All locks held by the transaction must be released after submission;

two-phase-locking

Although the use of locks can solve the problems caused by concurrent execution between different transactions, the use of two-phase locks introduces another serious problem, deadlock; different transactions waiting for each other's locked resources will cause death Lock, we give a simple example here:

deadlock

The two transactions acquired the locks on the draven and beacon resources at the beginning, and then a deadlock occurs when the other party has acquired the lock. There is no way for both parties to wait until the lock is released. If there is no deadlock processing mechanism, It will wait indefinitely, and neither transaction can be completed.

Deadlock handling

Deadlocks are often encountered in multithreaded programming. Once multiple threads are involved in competing for resources, it is necessary to consider whether the current threads or transactions will cause deadlocks; there are two ways to solve deadlocks. One is to prevent the occurrence and occurrence of deadlock from the source, and the other is to allow the system to enter a deadlock state, but when the system is deadlocked, it can be discovered and restored in time.

deadlock-handling

Prevent deadlock

There are two ways to help us prevent deadlocks, one is to ensure that there will be no loops in the waiting between transactions, that is, the waiting graph between transactions should be a directed acyclic graph , there is no cyclic waiting situation Or to ensure that all resources you want to obtain in a transaction are locked atomically at the beginning of the transaction, and all resources are either locked or not.

However, there are two problems with this approach. It is difficult to determine which resources need to be locked at the beginning of the transaction. At the same time, because some data that will be used very late is locked in advance, the utilization of data and the concurrency rate of transactions are also very high. The low. One solution is to lock all data rows in a certain order, and combine with the 2PL protocol to ensure that all data rows are locked in order from small to large in the locking phase, but this method still needs The transaction knows the data set to be locked in advance.

Another way to prevent deadlock is to use preemption plus transaction rollback to prevent deadlock. When the transaction starts to execute, a timestamp is first obtained. The database program will decide whether the transaction should wait or roll back according to the timestamp of the transaction. At this time, there are also two mechanisms for us to choose, one is the wait-die mechanism:

deadlock-prevention-wait-die

When the timestamp of the executed transaction is less than that of another transaction, that is, transaction A starts before B, then it will wait for another transaction to release the lock of the corresponding resource, otherwise it will keep the current timestamp and roll back.

Another mechanism is called wound-wait, which is a preemptive solution. It is completely opposite to the result of the wait-die mechanism. If the current transaction executes before another transaction and requests resources from another transaction, then another The transaction will be rolled back immediately, giving up resources to the transaction that is executed first, otherwise it will wait for other transactions to release resources:

deadlock-prevention-wound-wait

Both methods will cause unnecessary transaction rollbacks, which will bring a certain performance loss. The simpler way to solve the deadlock is to use the timeout period, but the setting of the timeout period needs to be carefully considered, otherwise it will cause The time-consuming transaction cannot be executed normally, or the deadlock that needs to be resolved cannot be found in time, so its use still has certain limitations.

Deadlock detection and recovery

If the database program cannot guarantee in principle that the deadlock will not occur through the protocol, then it needs to be detected in time when the deadlock occurs and restore from the deadlock state to the normal state to ensure that the database program can work normally. When using detection and recovery methods to solve deadlocks, database programs need to maintain reference information between data and transactions, and also need to provide an algorithm for judging whether the current database has entered a deadlock state, and finally need to be when a deadlock occurs Provide appropriate strategies for timely recovery.

In the previous section, we actually mentioned that the detection of deadlock can be judged by a directed wait graph. If a transaction depends on the data being processed by another transaction, then the current transaction will wait for the end of another transaction. This is also an edge in the entire waiting graph:

deadlock-wait-for-graph

As shown in the figure above, if a ring appears in this directed graph, it means that the current database has entered a deadlock state  TransB -> TransE -> TransF -> TransD -> TransB, and the deadlock recovery mechanism needs to be accessed at this time.

How to recover from a deadlock is actually very simple. The most common solution is to choose a transaction in the entire ring to roll back to break the ring in the waiting graph. There are three things to consider during the entire recovery process:

deadlock-recovery

Every time there is a deadlock, multiple transactions will be affected, and choosing which task to roll back is a must. The golden principle when choosing a victim (Victim) is to minimize the cost , so we need to integrate Consider factors such as the time the transaction has been calculated, the data rows used, and the transactions involved; when we choose the victim, we can start the rollback. There are actually two options for rollback: one is full rollback, the other is partial Rollback, partial rollback will be rolled back to a checkpoint before the transaction, if there is no checkpoint, there is no way to perform a partial rollback.

In the process of deadlock recovery, in fact, certain tasks may be selected as victims in multiple deadlocks, and they will not be successfully executed all the time, causing starvation. We need to ensure that the transaction will be in a finite time. Executed within, so the timestamp should be taken into consideration when choosing the victim.

Lock granularity

So far we have not discussed the locks of different granularities. We have been discussing data row locks for a long time, but in some cases we want to treat multiple nodes as a data unit, and use locks to directly connect this data unit, Tables and even databases are locked. The realization of this goal requires us to define locks of different granularities in the database:

granularity-hierarchy

When we have locks of different granularities, if a transaction wants to lock the entire database or the entire table, simply lock the corresponding node and add an explicit lock on the current node. Implicit locks are added to the nodes; although this type of lock with different granularities can solve the problem that when the parent node is locked, the child node cannot be locked, but we have no way to determine the parent immediately when the child node is locked. The node cannot be locked.

At this time, we need to introduce intention locks to solve this problem. When we need to lock the child nodes, first add the corresponding intention locks to all the parent nodes. The intention locks will not be mutually exclusive at all, just use To help the parent node quickly determine whether the node can be locked:

lock-type-compatibility-matrix

Here is the compatibility relationship between all locks after the introduction of two intention locks, intention shared locks and intention mutual exclusion locks ; up to this point, we have accelerated the throughput of the database through locks and intention locks of different granularities.

Optimistic concurrency control

In addition to the pessimistic concurrency control mechanism - the lock outside, we actually have other concurrency control mechanism, optimistic concurrency control (Optimistic Concurrency Control). Optimistic concurrency control is also called optimistic locking, but it is not a real lock. Many people mistakenly think that optimistic locking is a real lock, but it is just a concurrency control idea.

pessimistic-and-optimisti

In this section, we will first introduce the time-stamp-based concurrency control mechanism , and then expand on the basis of this protocol to achieve an optimistic concurrency control mechanism.

Timestamp-based protocol

The lock protocol is executed in sequence according to the time required by different transactions for the same data item, because the data that the subsequent transaction wants to acquire has been locked by the previous transaction, and can only wait for the release of the lock, so the order of transaction execution based on the lock protocol Related to the order in which locks are acquired. The timestamp-based protocol that I want to introduce here can determine the execution order of the transaction before the transaction is executed.

Every transaction has a globally unique timestamp. It can use the clock time of the system or a counter, as long as it can be guaranteed that all timestamps are unique and increase over time.

timestamp-ordering-protocol

The timestamp-based protocol can ensure that the order of parallel execution of transactions is exactly the same as the effect of serial execution of transactions according to timestamp; each data item has two timestamps, read timestamp and write timestamp, which respectively represent the current successful execution The timestamp of the transaction corresponding to the operation.

This protocol can ensure that all conflicting read and write operations can be executed serially according to the size of the timestamp. When performing the corresponding operation, you don't need to pay attention to other transactions and only need to care about the value of the timestamp corresponding to the data item:

timestamp-ordering-protocol-process

Whether it is a read operation or a write operation, the value of the read and write timestamp will be compared from left to right. If it is less than the current value, it will be directly rejected and then rolled back. The database system will add a new timestamp to the rolled back transaction and restart it. Perform this transaction.

Authentication-based protocol

Optimistic concurrency control is actually a verification-based protocol in essence, because in most applications, read-only transactions account for the vast majority. The possibility of conflicts between transactions due to write operations is very small, which means that most transactions are not The concurrency control mechanism is required to run very well, and the consistency of the database can also be guaranteed; and the concurrency control mechanism actually adds a lot of overhead to the entire database system, and we can actually reduce this part of the overhead through other strategies.

The verification protocol is the solution we found. It divides the execution of all transactions into two to three stages according to the read-only or update of the transaction:

validation-based-protoco

In the read phase, the database will perform all read and write operations in the transaction , and store all the written values ​​in temporary variables, and will not really update the content in the database; at this time, it will enter the next phase, the database program It will check whether the current changes are legal, that is, whether other transactions have updated the data during the RAED PHASE. If it passes the test, it will directly enter the WRITE PHASE and write all the changes in the temporary variables into the database. The transactions that have not passed the test will be Be terminated directly.

In order to ensure the normal operation of optimistic concurrency control, we need to know the occurrence time of different phases of a transaction, including the start time of the transaction, the start time of the verification phase, and the end time of the write phase; through these three timestamps, we can guarantee any conflicts Transactions will not be written to the database at the same time. Once a transaction completes the verification phase, it will be written immediately, and other transactions that read the same data will be rolled back and executed again.

As an optimistic concurrency control mechanism, it assumes that all transactions will eventually pass the verification phase and execute successfully, while the locking mechanism and timestamp-based sorting protocols are pessimistic, because they will force transactions to wait or return in the event of a conflict. Roll, even if there is no need for locks, it can ensure that there is no possibility of conflicts between transactions.

Multi-version concurrency control

The concurrency control mechanisms we have introduced so far are actually through delaying or terminating the corresponding transactions to resolve race conditions between transactions to ensure the serialization of transactions; although the previous two concurrency control mechanisms can indeed Fundamentally solve the serialization problem of concurrent transactions, but in the actual environment, most database transactions are read-only, and the read request is many times that of the write request. If there is no concurrency control mechanism before the write request and the read request, then The worst case is that the read request reads the data that has been written, which is completely acceptable for many applications.

multiversion-scheme

Under this premise, the database system introduces another concurrency control mechanism -  multi-version concurrency control (Multiversion Concurrency Control), every write operation will create a new version of the data, the data will be read from a finite number of versions Select the most suitable result in the file and return it directly; at this time, conflicts between read and write operations no longer need to be paid attention to, and management and quick selection of data versions have become the main problems that MVCC needs to solve.

MVCC is not a thing that opposes optimistic and pessimistic concurrency control. It can be well combined with the two to increase the concurrency of transactions. MVCC is implemented in the most popular SQL databases MySQL and PostgreSQL; but because They implement pessimistic locking and optimistic locking respectively, so the way MVCC is implemented is also different.

MySQL and MVCC

The multi-version two-phase lock protocol (Multiversion 2PL) implemented in MySQL combines the advantages of MVCC and 2PL. Each version of the data row has a unique timestamp. When there is a read transaction request, the database program will directly The return with the largest timestamp among multiple versions of data items.

multiversion-2pl-read

The update operation is slightly more complicated. The transaction will first read the latest version of the data to calculate the result of the data update, and then create a new version of the data. The timestamp of the new data is the largest version of the current data row  +1:

multiversion-2pl-write

The deletion of the data version is also selected based on the timestamp. MySQL will periodically clear the data with the lowest version from the database to ensure that there will not be a large amount of leftover content.

PostgreSQL and MVCC

Different from the use of pessimistic concurrency control in MySQL, optimistic concurrency control is used in PostgreSQL, which also leads to some differences in the implementation of MVCC in the combination of optimistic locking. The final implementation is called Multiversion Timestamp (Multiversion Timestamp). Ordering). In this protocol, all transactions are assigned a unique timestamp before execution, and each data item has two timestamps for reading and writing:

dataitem-with-timestamps

When a PostgreSQL transaction issues a read request, the database directly returns the latest version of the data without being blocked by any operation. When a write operation is being executed, the transaction timestamp must be greater than or equal to the read timestamp of the data row, otherwise Will be rolled back.

This implementation of MVCC ensures that the read transaction will never fail and does not need to wait for the lock to be released. For applications that have far more read requests than write requests, optimistic locking plus MVCC can greatly improve the performance of the database; This protocol can make some obvious performance improvements for some practical situations, but it will also cause two problems. One is that each read operation will update the read timestamp and cause two disk writes, and the second is the inter-transaction Conflicts are resolved through rollback, so if the possibility of conflict is very high or rollback is costly, the read and write performance of the database is not as good as the traditional lock wait method.

to sum up

The concurrency control mechanism of the database has a very mature and complete solution today. We do not need to design a new set of protocols to deal with conflicts between different transactions. We have learned from the concurrency control mechanism of the database. Relevant knowledge, whether it is locking or optimistic concurrency control, is widely used in other fields or applications, so it is necessary to understand and be familiar with the principles of different concurrency control mechanisms.

Guess you like

Origin blog.csdn.net/xintingandzhouyang/article/details/104998044