Easy-to-understand Mysql locks (global locks, table locks, row locks)

Preface

The original intention of the database design lock is to deal with concurrency issues. As a resource shared by multiple users, when concurrent access occurs, the database needs to reasonably control the access rules of the resource. The lock is an important data structure used to implement these access rules.

According to the scope of locking, the locks in MySQL can be roughly divided into three categories: global locks, table-level locks, and row locks.

Global lock

As the name implies, a global lock is to lock the entire database instance. Mysql provides a global read lock method, the command is:

Flush tables with read lock (FTWRL)

You can use the following command to unlock:

unlock tables;

This command can make the entire database in a readable state. Using this command will block the following operations:

1、数据更新语句(数据的增删改)
2、数据定义语句(包括建表、修改表结构等)
3、更新类事务的提交语句

Global lock usage scenarios

The typical use scenario of the global lock is to make a logical backup of the entire database. For example, master-slave replication means that all tables in the entire library are selected and saved as text.

One way is to use FTWRL to ensure that no other threads update the database, and then back up the entire database. Note that the entire library is completely read-only during the backup process.

Then, the danger of the database in a readable state:

  • If you back up on the main database, you can't perform updates during the backup period, and the business can basically stop.
  • If you are backing up on the slave database, the slave database cannot execute the binlog synchronized from the master database during the backup, which will cause the master-slave delay.

It seems that adding global locks is not good, so let's see what problems will occur if global locks are not added?

Without locking, the library obtained by the backup is not a logical point in time, and the view is logically inconsistent. In other words, because the backup cannot be performed at the same logical point in time, there may be some additions, deletions, and modifications during the backup process, which affects the consistency of the backup data.

The problem now is that you cannot get a consistent view without locking. Is there a solution?

The answer is yes, which is to start a transaction under the repeatable read isolation level.

The official logical backup tool that comes with it is mysqldump. When mysqldump uses the parameter -single-transaction, a transaction is started before the data is imported to ensure that the consistent view is obtained. Thanks to the support of MVCC, the data can be updated normally during this process.

With this logical backup tool, and then use the --single-transaction parameter, you can get a consistent view, and it does not affect the normal update of the data. How fragrant!

That's the problem. With the "True Fragrance" tool, what kind of FTWRL is used? Although it can make the entire library in a readable state, it will also block many normal operations. The premise of the "True Fragrance" tool is that the engine must support this isolation level. For example, for an engine such as MyISAM that does not support transactions, if there is an update during the backup process, only the latest data can always be obtained, which will destroy the consistency of the backup. At this time, we need to use the FTWRL command.

Therefore, the single-transaction method is only applicable to libraries that use the transaction engine for all tables. If some tables use engines that do not support transactions, then the backup can only be done through the FTWRL method.

There is another way to make the entire library in a read-only state, that is:

set global readonly=true

It is true that the readonly method can also make the entire library into a read-only state, but I still recommend you to use the FTWRL method for two main reasons:

1、在有些系统中,readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大,不建议你使用。
2、在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。

The business update is not just adding, deleting and modifying data (DML), it may also be operations such as adding fields and modifying the table structure (DDL). Either way, after a library is globally locked, if you want to add fields to any table in it, it will be locked.


.

Table lock

There are two types of table-level locks:

1、表锁
	表锁的语法是 lock tables … read/write
	与FTWRL类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。lock tables语法除了会限制别的线程的读写外,
	也限定了本线程接下来的操作对象。
	
	举个例子, 如果在某个线程A中执行lock tables t1 read, t2 write; 这个语句,则其他线程写t1、读写t2的语句都会被阻塞。
	同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作。连写t1都不允许,自然也不能访问其他表。

	在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令
	来控制并发,毕竟锁住整个表的影响面还是太大。

2、元数据锁(meta data lock,MDL)
	MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。
	
	在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
		(1)读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
		(2)读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

Next, let's look at how a transaction is executed?

We can see that session A starts first, and an MDL read lock will be added to this table at this time. Since session B also needs an MDL read lock, it can be executed normally.

After that, session C will be blocked because the MDL read lock of session A has not been released yet, and session C needs an MDL write lock, so it can only be blocked.

It doesn't matter if only session C itself is blocked, but all subsequent requests to apply for a new MDL read lock on this table will also be blocked by session C. As we said earlier, all the addition, deletion, modification, and query operations on the table need to apply for an MDL read lock first, and they are all locked, which means that this performance is completely unreadable.

If the query statement on a certain table is frequent and the client has a retry mechanism, that is to say, if a new session is requested after the timeout, the thread of this library will soon be full.

Therefore, the MDL lock in the transaction is applied at the beginning of the statement execution, but it will not be released immediately after the statement ends, but will be released after the entire transaction is committed.

We consider a question, how to do DDL safely?

首先要解决长事务,因为事务不提交就会一直占用MDL锁,可以查到当前执行中的事务。如果你要做DDL变更的表刚好有长事务在执行,
要考虑先暂停DDL,或者kill掉这个长事务。

但如果变更的是一个热点表,请求频繁,但由不得不DDL,该如何做呢??

这时候kill可能未必管用,因为新的请求马上就来了。比较理想的机制是,在alter table语句里面设定等待时间,如果在这个指定的等待时间
里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者DBA再通过重试命令重复这个过程。

Row lock

Above, we talked about MySQL's global locks and table-level locks, and let's talk about MySQL's row locks. We all know that Mysql's architecture is divided into three layers, Client, Server, Engine, and Mysql's row-level lock is implemented in the engine layer. Each engine layer has its own row lock implementation method. Of course, not all engines support row locks. Locks, such as: MyISAM does not support row-level locks. This is also one of the important reasons why MyISAM was replaced by InnoDB.

Next, let's talk about InnoDB's row locks and how to improve business concurrency by reducing lock conflicts.

As the name implies, row-level lock is actually easy to understand, that is, to lock a row of data in the table. When operation A needs to update a row of data, operation B also needs to update the same row of data, then operation B must wait until operation A is executed.

Below, we introduce a concept, two-stage lock.

Two-stage lock

What is a two-stage lock? As the name implies, the lock is divided into two phases, yes, it is divided into two phases: locking and unlocking, and locking must be before unlocking.

Let's look at an example to understand what a two-stage lock is. In the following operation, what will happen to the update statement of transaction B? Suppose the field id is the primary key of table t.

The key to this question is when transaction A releases the lock.

In InnoDB transactions, row locks are added when needed, but they are not released immediately when they are not needed, but only released when the transaction ends. This is the two-phase lock protocol.

Knowing this setting, how can it help us to use transactions? That is, if you need to lock multiple rows in your transaction, put the locks that are most likely to cause lock conflicts and most likely to affect concurrency as far as possible. for example.

假设你负责实现一个电影票在线交易业务,顾客A要在影院B购买电影票。我们简化一点,这个业务需要涉及到以下操作:

1、从顾客A账户余额中扣除电影票价;
2、给影院B的账户余额增加这张电影票价;
3、记录一条交易日志。

In order to ensure the safety of the operation, we have to put these three steps into one transaction, then how would you arrange the order of these three statements?

Imagine that if another customer C wants to buy a ticket at theater B at the same time, then the conflicting part of these two transactions is sentence 2. Because they want to update the balance of the same theater account, they need to modify the same row of data.

According to the two-phase lock protocol, no matter how you arrange the sequence of statements, all row locks required for operations are released when the transaction is committed. Therefore, if you arrange sentence 2 at the end, such as in the order of 3, 1, and 2, then the lock time of the theater account balance line will be the least. This minimizes lock waits between transactions and improves concurrency.

Now because of your correct design, the concurrency has been improved, but it still does not solve your troubles.

如果这个影院做活动,可以低价预售一年内所有的电影票,而且这个活动只做一天。于是在活动时间开始的时候,你的MySQL就挂了。你登上服务器一看,CPU消耗接近100%,但整个数据库每秒就执行不到100个事务。这是什么原因呢?

死锁与死锁检测

当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。这里我用数据库中的行锁举个例子。

这时候,事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁。 事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有两种策略:

1、直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
2、发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。

但是,我们又不可能直接把这个时间设置成一个很小的值,比如1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。

所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且innodb_deadlock_detect的默认值本身就是on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。

每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是O(n)的操作。假设有1000个并发线程要同时更新同一行,那么死锁检测操作就是100万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的CPU资源。因此,你就会看到CPU利用率很高,但是每秒却执行不了几个事务。

问题的症结在于,死锁检测要耗费大量的CPU资源。

You can consider reducing lock conflicts by changing one line to multiple logical lines. Taking the theater account as an example, you can consider putting it on multiple records, such as 10 records. The total amount of the theater account is equal to the sum of the values ​​of these 10 records. So every time you want to add money to the theater account, one of the records is randomly selected to add. In this way, the probability of each conflict becomes 1/10 of the original, which can reduce the number of lock waits and also reduce the CPU consumption of deadlock detection.

This scheme looks lossless, but in fact this type of scheme requires detailed design based on business logic. If the account balance may be reduced, such as the refund logic, then you need to consider that when a part of the line record becomes 0, the code needs special processing.


.

summary

This article talked about the three lock granularities of Mysql, global lock, table lock, and row lock. Among them, global locks and table locks are both Mysql-level locks, and row locks are engine-level locks.

Global lock, as the name implies, locks the entire library. Under what circumstances will the entire library be locked? Of course, when the entire database is backed up, we require the data to be read-only when backing up, because the data cannot be changed during the backup process, so you can use the FTWRL command to make the entire library in a read-only state. However, this method does not support additions, deletions, and DDL operations, and will block these behaviors. There is another way to make the entire library in a read-only state, which is to use the –single-transaction parameter of mysqldump, which not only allows the database to be in a read-only state, but also does not affect other series of operations of addition, deletion, and modification. Why not, but the prerequisite for using this tool is that all tables in the database must support the read-only transaction isolation level. Only when this condition is met can they be used. Otherwise, use the FTWRL command to back up.

There are two types of table-level locks: table locks and MDL locks. Before more fine-grained locks have appeared, table locks are the most common way to handle concurrency. Of course, if there are row-level locks supported by the InnoDB engine, table-level locks are generally not used. The smaller the granularity of the lock, the higher the degree of concurrency. Among them, MDL locks are divided into read locks and write locks, and read locks are not affected. Read locks and write locks, and write locks and write locks are mutually exclusive. You need to wait for one to execute the other before it can be executed.

行锁,不是所有的引擎都支持行级锁,像 MyISAM 引擎就不支持行级锁,这也是为什么 InnoDB 替代 MyISAM 的主要原因之一。行锁,就一个概念是两阶段锁,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。还一个概念是,死锁与死锁检测,出现死锁后,会有两种策略,一种是等待超时,另一种是,检测机制发现有死锁时,就回滚死锁链条中的某一个事务,让其他事务得以执行。当然第二种策略是比较合理的,但也有一些问题,比如:如果所有的事务都要进行死锁检测,死锁检测这个动作要耗费大量的CPU资源。文中提到了一个将一行改成逻辑上的多行来减少锁冲突的方案。但需要做详细的设计,还需要做一些特殊处理。

Guess you like

Origin blog.csdn.net/weixin_42653522/article/details/109553863