MySQL 锁的使用
大纲
MySQL 锁的介绍
锁的类型
在 MySQL 中,锁机制可以用来解决事务并发问题。MySQL 的锁有以下几种类型:
- 按锁的粒度可以分为:
- 行锁:锁住某行数据,锁粒度最小,并发度高。开销大,加锁慢,会出现死锁。
- 表锁:锁住整张表,锁粒度较大,并发度低。开销小,加锁快,不会出现死锁。
- 间隙锁:锁住的是一个区间。
- 全局锁:锁住整个数据库,锁粒度最大。
按锁的排它性可以分为:
- 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写。
- 排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写。
按锁的逻辑可以分为:
- 乐观锁:并不会真正的去锁住某行记录,而是通过一个版本号来控制数据的并发访问。
- 悲观锁:比如行锁、表锁等都是悲观锁。
锁的分类
MySQL 中常用的锁主要分为以下几类:
(1) 全局锁(Global Lock)
- 全局锁(Global Lock):用于在整个数据库级别上进行锁定操作,通常用于数据库备份和维护操作。加锁可以使用命令
FLUSH TABLES WITH READ LOCK;
,解锁可以使用命令UNLOCK TABLES;
。这种锁会将整个数据库置于只读模式,阻止所有写操作(包括数据更新、插入和删除),但允许读操作。常用于整个数据库备份时,以确保备份的一致性。
- 全局锁(Global Lock):用于在整个数据库级别上进行锁定操作,通常用于数据库备份和维护操作。加锁可以使用命令
(2) 表级锁(Table Locks)
- 表锁(Table Lock):将整张表锁定。
- 表共享锁(读锁):允许多个事务同时读取同一个表的数据,但不允许任何事务对表进行写操作。
- 表排它锁(写锁):只允许持有锁的事务对表进行读和写操作,不允许其他事务访问被锁定的表,既不允许读操作,也不允许写操作。
- 表锁(Table Lock):将整张表锁定。
(3) 行级锁(Row Locks)
- 行锁(Row Lock):锁定表中的特定行。行锁的粒度更小,允许更高的并发性。主要用于 InnoDB 存储引擎。
- 行共享锁(读锁):允许多个事务读取同一行数据,但不能修改。其他事务可以继续加共享锁,但不能加排它锁。
- 行排它锁(写锁):一个事务获取排它锁后,其他事务不能再读取或修改该行。排它锁会阻塞其他事务的共享锁和排它锁请求。
- 行锁(Row Lock):锁定表中的特定行。行锁的粒度更小,允许更高的并发性。主要用于 InnoDB 存储引擎。
(4) 意向锁(Intention Locks)
- 意向锁(Intention Lock):在表级别上设置的锁,用于表示某事务准备对某些行加共享锁或排它锁。
- 意向共享锁(IS Lock,Intention Shared Lock):事务准备加共享锁时,先在表上加一个意向共享锁。
- 意向排它锁(IX Lock,Intention Exclusive Lock):事务准备加排它锁时,先在表上加一个意向排它锁。
- 意向锁(Intention Lock):在表级别上设置的锁,用于表示某事务准备对某些行加共享锁或排它锁。
(5) 自增锁(AUTO-INC Locks)
- 自增锁(AUTO-INC Lock):用于处理包含
AUTO_INCREMENT
列的插入操作,确保自增列的值唯一性,主要用于 InnoDB 存储引擎。如果有事务回滚操作,数据会回滚,但自增序列不会回滚。
- 自增锁(AUTO-INC Lock):用于处理包含
(6) 间隙锁(Gap Locks)
- 间隙锁(Gap Lock):锁定一个范围,而不是锁定记录本身,主要用于防止幻读现象。间隙锁通常只有在可重复读(REPEATABLE READ)和串行化(SERIALIZABLE)隔离级别下才会使用到。
(7) 临键锁(Next-Key Locks)
- 临键锁(Next-Key Lock):是行锁和间隙锁的组合,锁定记录以及它前面的间隙,主要用于防止幻读现象。
(8) 插入意向锁(Insert Intention Locks)
- 插入意向锁(Insert Intention Lock):是一种特殊的间隙锁,多个事务在同一个间隙中插入记录时,不会互相阻塞。
间隙锁是什么
MySQL 的间隙锁(Gap Lock)是 InnoDB 存储引擎实现的锁机制之一,用于避免幻读现象并确保一致性。在可重复读(REPEATABLE READ)隔离级别下,间隙锁是必不可少的。间隙锁的主要特点和作用如下:
- (1) 锁定范围:间隙锁锁定的是记录之间的空隙。例如,在表中有两条记录 ID 分别为 10 和 20,间隙锁可以锁定 ID 在 10 和 20 之间的范围,使得该范围内不能插入新的记录。
- (2) 防止幻读:通过锁定记录之间的空隙,间隙锁防止了其他事务在这个空隙内插入新记录,从而避免了幻读现象。在幻读现象中,一个事务在执行多次相同查询时,看到的结果集不同,这是因为其他事务插入了新记录。
- (3) 实现方式:在 InnoDB 中,间隙锁通常与临键锁(Next-Key Lock)结合使用。临键锁是行锁和间隙锁的组合,用于锁定记录以及它前面的间隙,这样可以确保查询范围内的所有记录和间隙都被锁定。
- (4) 性能影响:间隙锁在提高一致性的同时,也会影响并发性能,因为它会锁定较大的范围,导致更多的等待和锁冲突。因此,在某些高并发应用中,需要权衡一致性和性能。
- (5) 事务隔离级别:间隙锁主要在可重复读(REPEATABLE READ)和串行化(SERIALIZABLE)隔离级别下使用。对于读已提交(READ COMMITTED)和读未提交(READ UNCOMMITTED)隔离级别,MySQL 通常不会使用间隙锁,因为这些隔离级别允许一定程度的并发和数据不一致。
总结
间隙锁通过锁定记录之间的空隙,可以防止其他事务在这个空隙(范围)内插入新记录,从而避免幻读现象。
比如,有一个带有索引的表 students
,其中包含两条记录 ID 分别为 10 和 20。一个事务执行以下查询:
1 | SELECT * FROM students WHERE id between 10 and 20 FOR UPDATE; |
此查询会通过间隙锁来锁定 ID 在 10 和 20 之间的间隙,所以其他事务不能在 ID 为 15 的位置插入新记录。
MySQL 锁的使用
行锁的使用
使用语法
行共享锁(读锁)允许多个事务读取同一行数据,但不能修改。其他事务可以继续加共享锁,但不能加排它锁。
1 | -- 加行共享锁 |
行排它锁(写锁)只允许持有锁的事务对一行数据进行读和写操作,其他事务不能再读取或修改该行。排它锁会阻塞其他事务的共享锁和排它锁请求。
1 | -- 加行排它锁 |
使用案例
假设有一张 accounts
表,其表结构如下:
1 | CREATE TABLE accounts ( |
如果希望防止脏读、不可重复读和幻读,可以使用事务和行锁来实现。
1 | -- 事务 1 |
表锁的使用
使用语法
表共享锁(读锁)允许多个事务同时读取同一个表的数据,但不允许任何事务对表进行写操作。
1 | -- 使用表共享锁 |
表排它锁(写锁)只允许持有锁的事务对表进行读和写操作,不允许其他事务访问被锁定的表,既不允许读操作,也不允许写操作。
1 | -- 使用表排它锁 |
使用案例
1 | -- 锁定表以进行写操作 |
MySQL 锁的注意事项
使用注意案例一
- 创建表
1 | CREATE TABLE `t_customer` ( |
- 操作步骤一(使用聚簇索引 - 主键索引)
MySQL 客户端一 | MySQL 客户端二 |
---|---|
BEGIN; UPDATE t_customer SET age=55 WHERE id=4; | |
UPDATE t_customer SET age=55 WHERE id=5; # 正常执行 或者 UPDATE t_customer SET age=33 WHERE id=6; # 正常执行 或者 UPDATE t_customer SET age=11 WHERE id=4; # 转圈等待 | |
COMMIT; | |
OK | |
注意:id 字段建立了主键索引 |
为什么上面的 MySQL 客户端二在执行第 3 个 UPDATE 操作时,会一直转圈等待呢?这是因为在
t_customer
表中,id
字段建立了主键索引,这导致了 MySQL 客户端一执行 UPDATE 操作时使用了行锁,间接导致 MySQL 客户端二的 UPDATE 操作要等待 MySQL 客户端一执行完成后才能执行。换言之,索引生效后,UPDATE 操作使用的就是行锁。
- 操作步骤二(使用非聚簇索引 - 普通索引)
MySQL 客户端一 | MySQL 客户端二 |
---|---|
BEGIN; UPDATE t_customer SET age=1 WHERE cname=’z3’; | |
UPDATE t_customer SET age=44 WHERE cNAME=’z4’; #正在执行 或者 UPDATE t_customer SET age=55 WHERE cNAME=’z5’; #正在执行 或者 UPDATE t_customer SET age=11 WHERE cNAME=’z3’ # 转圈等待 或者 UPDATE t_customer SET age=11 WHERE id=4 # 转圈等待 | |
COMMIT; | |
OK | |
注意:id 和 cname 字段都建立了普通索引 |
为什么上面的 MySQL 客户端二在执行第 3 和第 4 个 UPDATE 操作时,会一直转圈等待呢?这是因为在
t_customer
表中,cname
和id
字段都建立了索引,这导致了 MySQL 客户端一执行 UPDATE 操作时使用了行锁,间接导致 MySQL 客户端二的 UPDATE 操作要等待 MySQL 客户端一执行完成后才能执行。换言之,索引生效后,UPDATE 操作使用的就是行锁。
提示
更多关于聚簇索引与非聚簇索引的介绍,请看 这里 的教程。
使用注意案例二
- 创建表
1 | CREATE TABLE `t_customer` ( |
- 操作步骤
MySQL 客户端一 | MySQL 客户端二 |
---|---|
BEGIN; UPDATE t_customer SET age=55 WHERE phone=’13811112222’; | |
UPDATE t_customer SET age=55 WHERE id=5; # 转圈等待。 或者 UPDATE t_customer SET age=44 WHERE id=6; # 转圈等待。 | |
COMMIT; | |
OK | |
注意: phone 字段没有建立索引 |
为什么上面的 MySQL 客户端二在执行 UPDATE 操作时,会一直转圈等待呢?这是因为在
t_customer
表中,并没有给phone
字段加索引,这导致了 MySQL 客户端一执行 UPDATE 操作时使用了表锁(全表扫描),间接导致 MySQL 客户端二的 UPDATE 操作要等待 MySQL 客户端一执行完成后才能执行。换言之,MySQL 客户端二使用的行锁变成了表锁。
特别注意
在 UPDATE 操作中,如果 WHERE 后面的字段没有建立索引或者索引不生效,那么就会导致其他事务的 UPDATE 操作中的行锁变成表锁,查询性能大大降低。
使用注意案例的总结
InnoDB 的行锁,是通过锁住索引来实现的。如果加锁执行查询或者更新的时候,没有使用到索引,那么就会将整个聚簇索引(B+ 树)都锁住,这相当于锁表了,会大大降低数据库表的性能。