MYSQL 锁

1.1 什么是锁?

锁是一种用于保证在并发场景下每个事务仍能以一致性的方式读取和修改数据的方式,当一个事务对某一条数据上锁之后,其他事务就不能修改或者只能阻塞等待锁的释放,所以锁的粒度大小一定程度上可以影响到访问数据库的性能。

先创建一张test_lock表用于后续测试使用。

 CREATE TABLE `test_lock` ( `id` INT ( 20 ) NOT NULL, `name` VARCHAR ( 20 ) DEFAULT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8
test_lock表数据

1.2 都有哪些锁?

按照粒度大小: 全局锁、表锁、行锁

1.2.1 全局锁

全局锁就是对整个数据库进行加锁。在整库备份时会应用到全局锁。

MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。 存储引擎为InnoDB的话,可以使用官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数--single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性快照视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。

1.2.2 表锁

表锁就是对整张表进行加锁。MySQL表级锁有两种,一种是表锁,一种是元数据锁(meta data lock,MDL)。MySQL存储引擎MyISAM引擎中就只有表锁。

1.2.2.1 表锁:LOCK TABLE

MySQL提供了手动加锁的命令。

 LOCK TABLE 表名 READ;--锁定后表当前线程可以读,其他线程可以读,不能写。
 LOCK TABLE 表名 WRITE;--锁定后表当前线程可以读写,其他线程不可读写。
 UNLOCK TABLE; --解锁

而对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。

1.2.2.2 元数据锁:MDL

MDL作用是防止DDL和DML并发的冲突 ,保证读写的正确性

MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。

  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

1.2.3 行锁

行锁是锁住表中的一行数据,行锁一定是作用在索引上的。

我们可以思考下如果事务只是读数据,并不会对数据进行修改。无论读多少次,其他事务读到的数据不会改变。这个时候如果读数据和写数据使用同样的锁,对读事务来说性能上造成了浪费。因此,为了优化读数据,MySQL提供了共享锁(Shared Lock)和排它锁(Exclusive Lock)。

1.2.3.1 共享锁(S)
  • A shared (S) lock permits the transaction that holds the lock to read a row.

共享锁是行锁,又称为读锁,共享锁是一个事务并发读取某一行记录所需要持有的锁。

若果事务sessionA对 id=1的数据添加了S锁,那么其他事务sessionB、sessionC只能读id=1的数据,不能对该条数据添加其他锁。

MySQL提供了手动添加共享锁语法:

 select * from tables where id=1 lock in share mode;

加锁之后,直到加锁的事务结束(提交或者回滚)就会释放锁。

在下图1,三个会话sessionA、sessionB、sessionC,其中sessionA、sessionB先后开启事务手动添加共享锁,sessionA、sessionB都查询到了id=1的数据。sessionC添加了下一节要介绍的排它锁,在执行的时候会无法获取数据,直到超时(Lock wait timeout exceeded; try restarting transaction)或其它事务 commit后才可以查询到数据。

图1 事务共享锁执行顺序
1.2.3.2 排他锁(X)
  • An exclusive (X) lock permits the transaction that holds the lock to update or delete a row.

排它锁是行锁,又称为写锁,X锁。事务T1 为id=1添加排它锁,其他事务是无法更改id=1的数据。只能等待事务T1释放锁后才可以访问。一个事务在请求共享锁/排他锁时,获取到的结果却可能是行锁,也可能是间隙锁,也可能是临键锁,这取决于数据库的隔离级别以及查询的数据是否存在

MySQL提供了手动添加排他锁语法:

 select * from tables where id=1 for update;

在下图2中,会话sessionA、sessionB,开启事务后修改id=1的数据,InnoDB会为id=1添加排它锁。sessionA更新数据后没有添加,sessionB就开启了事务,也更新id=1的数据。此时就会出现死锁或者锁超时情况发生。

图2 事务排它锁执行顺序

按照图3所示执行SQL,sessionA开启表锁,sessionB开启行锁,结果可以查询到数据,我们可以知道InnoDB引擎表锁和行锁是可以共存的。

图3 表锁和行锁

我们思考下,除了共享锁(S)排它锁(X)之外,还需不需要其他行锁?共享锁(S)和排它锁(X)是否能满足所有的情况。

假设事务T1锁住了id=1的数据,让这行只能读,不能写。事务T2申请test_lock表上一个表锁。如果事务T2申请成功了,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。因此事务T2在申请表锁的时候,需要判断这张表是否被其他事务用表锁锁住,是否有其他行锁锁住。如果一张表有几百万数据,挨个遍历的话效率是非常低了。MySQL为解决这个问题,引入了意向锁

再比如执行下sessionA、sessionB中,sessionA开启共享锁,sessionB执行insert操作。sessionB如果执行成功的话,显然会造成sessionA出现不可重复读。怎么解决这种范围造成的问题呢?MYSQL引入了间隙锁

图4 范围查询

1.2.4 意向锁

Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table。

The intention locking protocol is as follows:

Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.

Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.

官网文档说,意向共享/排他锁是表锁,并且是取得共享锁/排他锁的前置条件。意向共享锁和意向排他锁都是系统自动添加和自动释放的,整个过程无需人工干预。意向锁有两大加锁规则:

  • 当需要给一行数据加上S锁的时候,MySQL会先给这张表加上IS锁。

  • 当需要给一行数据加上X锁的时候,MySQL会先给这张表加上IX锁。

共享锁/排他锁与意向共享锁/意向排他锁的兼容性关系:

锁之间的关系

1.2.5 间隙锁(Gap Lock)

上面啰嗦完MYSQL的表锁意向锁,加下来我们看下间隙锁到底是什么?

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record。

Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.

通过官网介绍,我们了解到间隙锁是一个开区间,比如(5, +)。本质上是不会区分共享间隙锁,排他间隙锁,且间隙锁是不互斥的,即两个事务可以同时持有包含共同间隙的间隙锁。

这里的共同间隙包括两种场景:其一是两个间隙锁的间隙区间完全一样;其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。间隙锁本质上是用于阻止其他事务在该间隙内插入新记录,而自身事务是允许在该间隙内插入数据的。也就是说间隙锁的应用场景包括并发读取、并发更新、并发删除和并发插入。

在MySQL官网上关于间隙锁还有一段重要描述:

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED. Under these circumstances, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.

这段话表明,在RU和RC两种隔离级别下,即使你使用select ... in share mode或select ... for update,也无法防止幻读(读后写的场景)。因为这两种隔离级别下只会有行锁,而不会有间隙锁。这也是为什么示例中要规定隔离级别为RR的原因。

1.2.6 临键锁(Next-key Locks)

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

临键锁就是记录锁和间隙锁的结合。即临键锁是是一个左开右闭的区间,比如(5,18]。

临键锁在以下两个条件时会降级成为间隙锁或者记录锁:

  • 当查询未命中任务记录时,会降级为间隙锁。

  • 当使用主键或者唯一索引命中了一条记录时,会降级为记录锁。

1.2.6.1 临键锁加锁规则

临键锁的划分是按照左开右闭的区间来划分的,也就是我们可以把test_lock表中的记录划分出如下区间:(-∞,1],(1,2],(2,3],(3,8],(8,10],(10,15],(15,+∞)。

下图中sessionA查询涉及到的区间为(3,8],(8,10],(10,15]。按照左开右闭原则,14无法插入,15可以插入。

图5:间隙锁验证

1.2.7 插入意向锁(Insert Intention Locks)

在mysql官方文档中,有以下描述:

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

上面的意思是插入意向锁是一种特殊的间隙锁,专门针对的是数据行的插入(insert)操作,多个事务插入相同的索引间隙时,只要不是插入到相同的位置,则不需要进行锁等待。假设有索引记录的值分别是4和7,单独的事务分别尝试插入5和6,在获得插入行的排它锁之前,每个事务都是用插入意向锁来锁定4和7之间的空间,但是不会相互阻塞。因为行级别是没有冲突的。单理解就是插入意向锁锁定了索引之间的间隙,但是插入意向锁之间没有互相阻塞。

图6:插入意向锁

1.2.8 死锁

到此为止,我们介绍了MySQL常用的锁。结合前面说的锁的内容,我们先来分析下下面的场景:

test_lock表数据如下

test_lock表数据
图7:死锁

在上图中,因为IX锁是表锁且IX锁之间是兼容的,因而事务A和事务B都能同时获取到IX锁和间隙锁。另外,需要说明的是,因为我们的隔离级别是RR,且在请求X锁的时候,查询的对应记录都不存在,因而返回的都是间隙锁。接着事务A请求插入意向锁,这时发现事务B已经获取了一个区间间隙锁,而且事务A请求的插入点在事务B的间隙锁区间内,因而只能等待事务B释放间隙锁。这个时候事务B也请求插入意向锁,该插入点同样位于事务A已经获取的间隙锁的区间内,因而也不能获取成功,不过这个时候,MySQL已经检查到了死锁,于是事务B被回滚,事务A提交成功。

你可能感兴趣的:(MYSQL 锁)