锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,除传统的计算资源(CPU、RAM、I/O)争用外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
MySQL 中的锁可以分为以下三类:
- 全局锁:锁定数据库中所有的表
- 表级锁:每次操作锁住整张表
- 行级锁:每次操作锁住表中对应的行数据
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例处于只读状态,后续的 DML、DDL 语句都会无法执行,典型的使用场景就是做全库的备份,对所有表进行锁定,从而获取一致性视图,保证数据的完整性。
打开全局锁:
flush tables with read lock;
备份指定数据库,将 crm 数据库备份到当前命令行目录的 crm.sql 文件中,不是在 MySQL 的命令行中执行,可直接在 Windows 命令行执行即可。
mysqldump -u root -p crm > crm.sql
释放全局锁:
unlock tables;
在数据库中加全局锁,是一个比较重的操作,会存在以下问题:
1、如果在主库上备份,那么备份期间都不能执行更新操作,业务基本停滞。
2、如果在从库上备份,那么备份期间从库不能执行主库同步过来的二进制日志(binlog),导致主从延迟。
在 InnoDB 引擎中,我们可以在备份时添加参数--single-transaction
来实现不加锁的一致性数据备份:
mysqldump --single-transaction -u root -p crm > crm.sql
表级锁
每次操作锁住整张表,锁定力度大,发生锁冲突的概率高,并发度最低。表级锁主要分一下几类:
- 表锁
- 元数据锁(meta data lock,MDL)
- 意向锁
表锁
表锁可以分为两类:
1、表共享读锁
(read lock),当前客户端对表加锁后和其它客户端只能读表中的数据,当前客户端对表的写操作直接报错,其它客户端的写操作会被阻塞,直到锁被释放后才继续执行。
2、表独占写锁
(write lock),当前客户端对表加锁后其它客户端对表的读写操作将会被阻塞,直到锁被释放后才会继续执行
表锁的操作语法如下:
-- 加锁
lock tables 表名... read/write
-- 释放锁(直接关闭客户端连接也可以释放锁)
unlock tables
元数据锁(MDL)
元数据可以简单的理解为表结构,元数据锁的加锁是系统自动控制的,无需显示指定,在访问一张表的时候会自动加上,该锁主要作用是维护表结构数据的一致性,在表上有没提交的事务时,对表结构的更新操作会处于阻塞状态。
MySQL5.5 开始引入了 MDL,在对一张表进行增删改查的时候,会自动加上 MDL 读锁(共享锁);在对表结构进行变更操作时,会加上 MDL 写锁(排它锁)。
意向锁
当对表中数据执行 DML 时会锁定行数据,此时如果其它客户端要对表加表锁,则需要检查表中每一行数据是否加锁了。为了避免在执行 DML 语句的时候,加的行锁与表锁冲突,在 InnoDB 中引入了意向锁,使得表锁不用检查每行数据是否加锁,减少了表锁的检查,提高了效率。
有了意向锁后,在执行 DML 时,先添加行数据锁,然后对整张表添加意向锁,其它客户端在添加表锁时先检查意向锁,如果意向锁和要添加的表锁是兼容的则添加成功,否则添加表锁的操作会被阻塞。
意向锁可以分为以下两类:
1、意向共享锁(IS),由语句 select ... lock in share mode 添加,与表锁中的表共享读锁兼容,与表独占写锁互斥。
2、意向排它锁(IX),由语句 insert、update、delete、select ... for update 添加,与表锁中的表共享读锁、表独占写锁都互斥。意向锁之间不会互斥。
行级锁
每次操作锁住对应的数据行,锁定粒度最小,发生冲突的概率最低,并发程度高,主要用在 InnoDB 存储引擎中。InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加锁的。
行级锁主要分为以下几类:
1、行锁(Record Lock):锁定单个行记录的锁,防止其他事务对其进行 update、delete,在
read committed、repeatable read 两种隔离级别下都支持。
2、间隙锁(Gap Lock):锁定索引记录左右的间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 insert 产生幻读,在 repeatable read 隔离级别下支持。
3、临键锁(Next-Key Lock):行锁和间隙锁的组合,同时锁住数据和数据前面的间隙,在 repeatable read 隔离级别下支持。
行锁
在 InnoDB 中实现了两类行锁:
1、共享锁(s):允许获得共享锁的事务读取一行数据,允许其他事务获得相同数据集的共享锁,但阻止获得排它锁。共享锁和排它锁是互斥的。
2、排它锁(x):允许获取排它锁的事务更新一行数据,阻止其它事务获得相同数据集的共享锁和排它锁。排它锁和其它锁都是互斥的。
常见 SQL 操作对应的行锁类型:
SQL | 行锁类型 | 说明 |
---|---|---|
insert | 排它锁 | 自动加锁 |
update | 排它锁 | 自动加锁 |
delete | 排它锁 | 自动加锁 |
select | 不加锁 | |
select ... lock in share mode | 共享锁 | 需要手动在 select 后添加 lock in share mode |
select ... for update | 排它锁 | 需要手动在 select 后添加 for update |
默认情况下,InnoDB 在 repeatable read 事务隔离级别下运行,并使用临键锁进行搜索和索引扫描来防止幻读。
1、针对唯一索引进行检索时,对已存在的记录进行等值匹配时, 临键锁会自动优化为行锁。
2、InnoDB 的行锁是针对索引加的锁,如果不通过索引字段检索数据,那么 InnoDB 会将表中的所有记录加锁,此时会升级为表锁,影响效率。
可以通过以下 SQL 查看意向锁以及行锁的加锁情况:
select object_schema, object_name, index_name, lock_type, lock_mode, lock_data from performance_schema.data_locks;
间隙锁、临键锁
间隙锁唯一的目是防止其它事务插入间隙,间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
默认情况下,InnoDB 在 repeatable read 事务隔离级别下运行,并使用临键锁锁进行搜索和索引扫描来防止幻读。
1、索引上的等值查询(唯一索引),给不存在的记录加锁时,临键锁优化为间隙锁。
2、索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询条件时,临键锁退化为间隙锁。
3、索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止。