MySQL数据库——锁机制

1 认识锁机制

在认识锁机制前,首先思考一个问题:在同一时刻,用户A和用户B同时要获取并修改sh_goods表中id等于2的stock库存量值,此时会发生什么呢?

假设在初始情况下,sh_ goods表中id等于2的stock库存量值为500。

在不添加锁的前提下,用户A关闭自动提交,将stock的值修改为300,然后查询当前stock值为300(修改但未提交);与此同时用户B也获取stock,它的值却为500。当用户A提交了修改后,用户B获取到的值又变为300。整个操作过程出现了两个大的问题,一是用户B第1次查询stock字段的值与用户A不同,二是用户B前后两次读取的stock值不同,从而产生了用户并发操作时数据不一 致的情况。

解决办法就是,在用户A和用户B同时向sh_goods表发出请求操作时,根据系统内部设定的操作优先级(获取数据优先或修改数据优先的原则),锁住指定用户(如A)要操作的资源(sh_goods 表),同时让另外一个用户(如B)排队等候,直到锁定资源的用户(如A)操作完成,并释放锁后,再让另一个用户(如B)对资源进行操作。其中,对资源加锁的方式,可以采用修改事务隔离级别(前面章节已讲)的方式实现。

简单地说,锁机制就是为了保证多用户并发操作时,能使被操作的数据资源保持一致性的设计规则。又因MySQL数据库自身设计的特点,利用多种存储引擎处理不同特定的应用场景,所以锁机制在不同存储引擎中的表现也有一-定的区别。

根据存储引擎的不同,MySQL中常见的锁有两种,分别为表级锁(如MyISAM、MEMORY存储引擎)和行级锁(如InnoDB存储引擎)。另外InnoDB存储引擎中还含有表级锁,具体内容会在后面的小节详细讲解,此处了解即可。

表级锁是MySQL中锁的作用范围(锁的粒度)最大的一种锁,它锁定的是用户操作资源所在的整个数据表,有效地避免了死锁的发生,且具有加锁速度快、消耗资源小的特点。

正所谓事物都有两面性,表级锁的优势同样给它带来了一定的缺陷,因其锁定的粒度大,在并发操作时发生锁冲突的概率也大。

行级锁是MySQL中锁的作用范围最小的一种锁,它仅锁定用户操作所涉及的记录资源,有效地减少了锁定资源竞争的发生,具有较高处理并发操作的能力,提升系统的整体性MySQL数据库原理、设计与应用能。但同时也因其锁定的粒度过小,每次加锁和解锁所消耗的资源也会更多,发生死锁的可能性更高。

另外,根据锁在MySQL中的状态也可将其分为“隐式”与“显式”。所谓“隐式”锁指的是MySQL服务器本身对数据资源的争用进行管理,它完全由服务器自动执行。而“显式”锁指的是用户根据实际需求,对操作的数据显式地添加锁,同样在使用完数据资源后也需要用户对其进行解锁。

小提示:在了解死锁前,首先要理解什么是锁等待。所谓锁等待指的是一个用户(线程)等待其他用户(线程)释放锁的过程。而死锁可以简单地理解为两个或多个用户(线程)在互相等待对方释放锁而出现的一种“僵持”状态,若无外力作用,它们将永远处于锁等待的状态,此时就可以说系统产生了死锁或处于死锁状态。

2 表级锁

在实际应用中,表级锁根据操作的不同可以分为读锁和写锁。读锁表示用户读取(如SELECT查询)数据资源时添加的锁,此时其他用户虽然不可以修改或增加数据资源,但是可以读取该数据资源,因此读锁也可以称为共享锁;而写锁表示用户对数据资源执行写(如INSERT,UPDATE.DELETE等)操作时添加的锁,此时除了当前添加写锁的用户外,其他用户都不能对其进行读/写操作,因此写锁也可以称为排他锁或独占锁。

MyISAM存储引擎表是MySQL数据库中最典型的表级锁,下面就以此存储引擎的表级锁为例详细讲解“隐式”读/写的表级锁和“显式”读/写表级锁的添加。

1.“隐式”读/写的表级锁

当用户对MyISAM存储引擎表执行SELECT查询操作前,服务器会“自动”地为其添加一个表级的读锁,执行INSERT.UPDATE.DELETE等写操作前,服务器会“自动”地为其添加一个表级的写锁;直到查询完毕,服务器再“自动”地为其解锁。执行时间可以看作是“隐式”表级锁读/写的生命周期,且该生命周期的持续时间一般都比较短暂。

默认情况下,服务器在“自动”添加“隐式”锁时,表的更新操作优先级高于表的查询操作。在添加写锁时,若表中没有任何锁则添加,否则将其插人到写锁等待的队列中;在添加读锁时,若表中没有写锁则添加,否则将其插人到读锁等待的队列中。

2.“显式”读/写的表级锁

在实际应用中,可以根据开发需求,对要操作的数据表进行“显式”地添加表级锁。其基本语法格式如下。

LOCK TABLES 数据表名 READ [LOCAL]| WRITE,

在上述语法中,LOCKTABLES可以同时锁定多张数据表。READ表示表级的读锁,添加此锁的用户可以读取该表但不能对此表进行写操作,否则系统会报错;此时其他用户可以读取此表,若执行对此表的写操作则会进入等待队列。WRITE 表示表级的写锁,添加此锁的用户可以对该表进行读/写操作,在释放锁之前,不允许其他用户访问与操作。

需要注意的是,在为MyISAM存储引擎表设置“显式”读锁时,若添加LOCAL关键字,则在不发生锁冲突的情况下,未添加此锁的其他用户可以在表的末尾实现并发插人数据的功能。

此外,对于表级锁来说,虽然锁本身消耗的资源很少,但是锁定的粒度却很大,当多个用户访问时,会造成锁资源的竞争,降低了并发处理的能力。因此,从数据库优化的角度来考虑,应该尽量减少表级锁定的时间,进而提高多用户的并发能力。此时,对于用户添加的“显式”表级锁,需要使用MySQL提供的UNLOCKTABLES语句释放锁。

值得一提的是,用户设置的“显式”表级锁仅在当前会话内有效,若会话期间内未释放锁,在会话结束后也会自动释放。

为了读者更好地理解,下面通过一个具体的案例进行演示。具体步骤如下。
(1)创建MyISAM表并插人2条测试数据。

mysql> CREATE TABLE mydb.table_lock(id int)ENGINE=MyISAM;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO mydb.table_lock VALUES(1),(2);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

(2)设置“显式”读的表级锁。
打开两个客户端A和B,在客户端A中为mydb.table_lock设置“显式”读的表级锁后,然后分别在客户端A和客户端B中执行SELECT和UPDATE操作。具体SQL语句及执行结果如下。

# ① 在客户端A中添加表级读锁
mysql> LOCK TABLE mydb.table_lock READ;
Query OK, 0 rows affected (0.00 sec)
# ② 在客户端A中执行SELECT和UPDATE操作
mysql> SELECT * FROM mydb.table_lock \G
*************************** 1. row ***************************
id: 1
*************************** 2. row ***************************
id: 2
2 rows in set (0.00 sec)
mysql> UPDATE mydb.table_lock SET id = 3 WHERE id = 1;
ERROR 1099 (HY000): Table 'table_lock' was locked with a READ lock and can't be updated
mysql> SELECT * FROM mydb.mt \G
ERROR 1100 (HY000): Table 'mt' was not locked with LOCK TABLES
# ③ 在客户端B中执行SELECT和UPDATE操作
mysql> SELECT * FROM mydb.table_lock \G
*************************** 1. row ***************************
id: 1
*************************** 2. row ***************************
id: 2
2 rows in set (0.00 sec)
mysql> UPDATE mydb.table_lock SET id = 3 WHERE id = 1;
# 此处光标会不停闪烁,进入锁等待状态
# ④ 在客户端A中释放锁
mysql> UNLOCK TABLES;
Query OK, 0 rows affected (0.00 sec)
# ⑤ 客户端B在客户端A释放锁后,会立即执行③中等待的写锁
mysql> UPDATE mydb.table_lock SET id=3 WHERE id=1;
Query OK, 1 row affected (5.64 sec)
Rows matched: 1  Changed: 1  Warnings: 0

从以上的操作可以看出,添加表级读锁的客户端A仅能对mydb.table_lock执行读取操作,不能执行写操作,也不能操作其他未锁定的数据表,如mydb.mt。对于未添加锁的客户端B则可以执行SELECT操作,但是执行UPDATE操作则会进入锁等待状态,只有客户端A结束会话或执行UNLOCK TABLES释放锁时,客户端B的操作才会被执行。具体SQL语句及执行结果如下。

# ① 在客户端A中添加表级读锁
mysql> LOCK TABLE mydb.table_lock READ LOCAL;
Query OK, 0 rows affected (0.00 sec)
# ② 在客户端B中,插入一条记录
mysql> INSERT INTO mydb.table_lock VALUES(4);
Query OK, 1 row affected (0.00 sec)

(3)并发插人操作。
在MyISAM存储引擎的数据表中,还支持并发插入操作,用于减少读操作与写操作对表的竞争情况。实现语法为LOCK… READ LOCAL,具体SQL语句及执行结果如下。

# ① 在客户端A中添加表级读锁
mysql> LOCK TABLE mydb.table_lock READ LOCAL;
Query OK, 0 rows affected (0.00 sec)
# ② 在客户端B中,插入一条记录
mysql> INSERT INTO mydb.table_lock VALUES(4);
Query OK, 1 row affected (0.00 sec)

从上述执行结果可知,即使客户端A中已添加了表级读锁,在未释放此读锁时,在客户端B中依然可以实现数据插人操作,此操作也称为并发插人。
需要注意的是,并发插人的数据不能是DELETE操作删除的记录,并且只能在表中最后的一行记录后继续增加新记录。
(4)设置“显式”写的表级锁。

# ① 在客户端A中添加表级写锁
mysql> LOCK TABLE mydb.table_lock WRITE;
Query OK, 0 rows affected (0.00 sec)
# ② 在客户端A中执行更新和查询操作
mysql> UPDATE mydb.table_lock SET id = 1 WHERE id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> SELECT * FROM mydb.table_lock \G
*************************** 1. row ***************************
id: 3
*************************** 2. row ***************************
id: 1
*************************** 3. row ***************************
id: 4
3 rows in set (0.00 sec)
# ③ 在客户端B中执行查询操作
mysql> SELECT * FROM mydb.table_lock;
# 此处光标会不停闪烁,进入锁等待状态

从上述操作可以看出,添加了写锁的用户,可以执行读/写操作(如增删改查),而其他用户不论执行任何操作(如SELECT),都只能处于等待状态,直到写锁被释放,才能够执行。

3 行级锁

InnoDB存储引擎的锁机制相对于MyISAM存储引擎的锁复杂一些,原因在于它既有表级锁又有行级锁。其中,InnoDB表级锁的应用与MyISAM表级锁的相同,这里不再赘述。那么InnoDB存储引擎的表什么时候添加表级锁,什么时候添加行级锁呢?只有通过索引条件检索的数据InnoDB存储引擎才会使用行级锁,否则将使用表级锁。

InnoDB的行级锁根据操作的不同也分为共享锁和排他锁。为了读者更好地理解,下面以“隐式”的行级锁和“显式”的行级锁为例进行详细讲解。

1.“隐式”行级锁
当用户对InnoDB存储引擎表执行INSERT.UPDATE.DELETE等写操作前,服务器会“自动”地为通过索引条件检索的记录添加行级排他锁;直到操作语句执行完毕,服务器再“自动”地为其解锁。

而语句的执行时间可以看作是“隐式”行级锁的生命周期,且该生命周期的持续时间一般都比较短暂。通常情况下,若要增加行级锁的生命周期,最常使用的方式是事务处理,让其在事务提交或回滚后再释放行级锁,使行级锁的生命周期与事务的相同。

为了读者更好地理解,下面在事务中演示“隐式”行级锁的使用。具体步骤如下。

(1)创建InnoDB表并插人测试数据。

mysql> CREATE TABLE mydb.row_lock (
    ->   id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    ->   name VARCHAR(60) NOT NULL,
    ->   cid INT UNSIGNED,
    ->   KEY cid (cid)
    -> )DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO mydb.row_lock (name, cid) VALUES ('铅笔', 3),
    -> ('风扇', 6), ('绿萝', 1), ('书包', 9), ('纸巾', 20);
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

(2)设置“隐式”行级锁。
打开两个客户端A和B,在客户端A中为mydb.row_lock设置“隐式”行级的排他锁后,然后在客户端B中执行SELECT和DELETE操作。具体SQL语句及执行结果如下。

# ① 在客户端A中,修改cid等于3的name值
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE mydb.row_lock SET name = 'cc' WHERE cid = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# ② 在客户端B中,删除cid等于2和3的记录
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM mydb.row_lock WHERE cid = 2;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM mydb.row_lock WHERE cid = 3;
# 此处光标会不停闪烁,进入锁等待状态
# ③ 在客户端A和B中,回滚以上的操作
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

从以上执行结果可知,一个客户端对InnoDB表执行UPDATE操作时,对符合索引条件的记录会隐式地添加一个行级锁,与此同时其他用户不能再执行写操作,但可以操作不符合索引条件的记录(如删除cid等于2的记录)。

2.“显式"行级锁

对于InnoDB表来说,若要保证当前事务中查询出的数据不会被其他事务更新或删除,利用普通的SELECT语句是无法办到的,此时需要利用MySQL提供的“锁定读取”的方式为查询操作显式地添加行级锁。其基本语法格式如下。

SELECT 语句 FOR UPDATE|LOCK IN SHARE MODE

在上述语法中,只需在正常的SELECT语句后添加FOR UPDATE或LOCK IN SHARE MODE即可实现“锁定读取”,前者表示在查询时添加行级排他锁,后者表示在查询时添加行级共享锁。

用户在向InnoDB表显式添加行级锁时,InnoDB存储引擎首先会“自动”地向此表添加一个意向锁,然后再添加行级锁。此意向锁是一个隐式的表级锁,多个意向锁之间不会产生冲突且互相兼容。意向锁是由MySQL服务器根据行级锁是共享锁还是排他锁,自动添加意向共享锁或意向排他锁,不能人为干预。

意向锁的作用就是标识表中的某些记录正在被锁定或其他用户将要锁定表中的某些记录。相对行级锁,意向锁的锁定粒度更大,用于在行级锁中添加表级锁时判断它们之间是否能够互相兼容。好处就是大大节约了存储引擎对锁处理的性能,更加方便地解决了行级锁与表级锁之间的冲突。

为了读者更好地理解,下面通过一个表格展示表级的共享/排他锁与意向共享/排他锁之间的兼容性关系,具体如表所示。

表 表级共享/排他锁与意 向共享/排他锁之间的关系

表级共享锁 表级排他锁 意向共享锁 意向排他锁
表级共享锁 兼容 冲突 兼容 冲突
表级排他锁 冲突 冲突 冲突 冲突
意向共享锁 兼容 冲突 兼容 兼容
意向排他锁 冲突 冲突 兼容 兼容

需要注意的是,InnoDB表中当前用户的意向锁若与其他用户要添加的表级锁冲突时,有可能会发生死锁而产生错误。
接下来利用上面创建的mydb.row_lock表,演示添加行级排他锁时客户端A和客户端B执行SQL语句的状态,具体步骤如下。

# ① 在客户端A中,为cid等于3的记录添加行级排他锁
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM mydb.row_lock WHERE cid = 3 FOR UPDATE;
+----+------+------+
| id | name | cid  |
+----+------+------+
|  1 | 铅笔  |    3 |
+----+------+------+
1 row in set (0.00 sec)
# ② 在客户端B中,为cid等于2的记录添加隐式行级排他锁,设置表级排他锁
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE mydb.row_lock SET name = 'lili' WHERE cid = 2;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
mysql> LOCK TABLE mydb.row_lock READ;
# 此处光标会不停闪烁,进入锁等待状态
# ③ 回滚以上的操作并释放表级锁

从以上的执行结果可知,在客户端A中为cid等于3的记录添加行级排他锁后,在客户端B中,可以为除cid等于3外的记录添加行级排他锁(如cid等于2的隐式排他锁),但是在为表添加表级共享锁时会发生冲突,进行锁等待状态。

此外,默认的情况下,当InnoDB处于REPEATABLE READ(可重复读)的隔离级别时,行级锁实际上是一个next-key锁,它是由间隙锁(gaplock)和记录锁(recordlock)组成的。其中,记录锁(recordlock)就是前面讲解的行锁;间隙锁指的是在记录索引之间的间隙、负无穷到第1个索引记录之间或最后1个索引记录到正无穷之间添加的锁,它的作用就是在并发时防止其他事务在间隙插入记录,解决了事务幻读的问题。

为了读者更好地理解,下面为mydb.row_lock表添加行锁,查看间隙锁是否存在。具体步骤如下。

# ① 在客户端A中,为cid等于3的记录添加行锁
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM mydb.row_lock WHERE cid=3 FOR UPDATE;
+----+------+------+
| id | name | cid  |
+----+------+------+
|  1 | 铅笔  |    3 |
+----+------+------+
1 row in set (0.00 sec)
# ② 在客户端B中,插入cid等于1、2、5、6的记录
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('电视', 1);
# 此处光标会不停闪烁,进入锁等待状态
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('电视', 2);
# 此处光标会不停闪烁,进入锁等待状态
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('电视', 5);
# 此处光标会不停闪烁,进入锁等待状态
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('电视', 6);
Query OK, 1 row affected (0.00 sec)

在上述操作中,客户端A在cid等于3的记录中添加了行锁,理论上其他用户在并发时可以插入除cid等于3的任意记录,但是因为间隙锁的存在,服务器也会锁定当前表中cid(值分别为1、3、6、9、20)值为3的记录左右的间隙,间隙的区间范围为[1 ,3)和[3,6)。

值得一提的是,在执行SELECT-FOR UPDATE时,若检索时未使用索引,则InnoDB存储引擎会给全表添加一个表级锁,并发时不允许其他用户进行插人。另外,若查询条件使用的是单字段的唯一性索引,InnoDB存储引擎的行级锁不会设置间隙锁。

间隙锁的使用虽然解决了事务幻读的情况,但是也会造成行锁定的范围变大,若在开发时想要禁止间隙锁的使用,可以将事务的隔离级别更改为READ COMMITTED(读取提交)。

多学一招:查看InnoDB表的锁
InnoDB存储引擎的锁比较复杂,读者可以在添加一个行锁后,使用SHOW ENGINE INNODB
STATUS语句查看当前表中添加的锁的类型。另外,在查看时要保证开启系统变量innodb_status_output_locks
才能获取锁定的信息。例如,查看mydb.row_lock 表添加的锁,部分信息如下。

 TABLE LOCK table `mydb`.`row_lock` trx id 10386 lock mode IX RECORD LOCKS space id 247 page no 4 n bits 80 index cid of table `mydb`.`row_lock` trx id 10386 lock_mode X 
 ︙(此处省略部分内容)
  RECORD LOCKS space id 247 page no 3 n bits 80 index PRIMARY of table `mydb`.`row_lock` trx id 10386 lock_mode X locks rec but not gap
 ︙(此处省略部分内容)  
 RECORD LOCKS space id 247 page no 4 n bits 80 index cid of table ``mydb`.`row_lock` trx id 10386 lock_mode X locks gap before rec
 ︙(此处省略部分内容) 

在上述信息中,“IX”表示mydb. row_ lock 中添加了一个意向排他锁,“X”表示next-key lock的排他锁,“X
locks rec but not gap”表示记录锁,“X locks gap before rec”表示间隙锁。
它们之间的关系为“IX”在“X"之前添加,而“X”是由“X locks rec but not gap" 和“X locks gap
before rec”组成的。


超全面的测试IT技术课程,0元立即加入学习!有需要的朋友戳:


腾讯课堂测试技术学习地址

欢迎转载,但未经作者同意请保留此段声明,并在文章页面明显位置给出原文链接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章