- 读锁:S 锁、共享锁
- 写锁:X 锁、独占锁
读读共享,写写互斥、读写互斥
即:
读锁 | 写锁 | |
---|---|---|
读锁 | 兼容 | 互斥 |
写锁 | 互斥 | 互斥 |
全局锁(库级锁)
使用
加锁:
flush tables with read lock
执行后,整个数据库处于只读状态,这时写操作都会被阻塞:
- 对数据的增删改操作,如
insert
、delete
、update
等 - 对表结构的更改操作,如
alter table
、drop table
等
释放全局锁:
unlock tables
另外,当会话退出后,也会释放全局锁
应用场景
全局锁主要应用于做全库逻辑备份,在备份期间,不会因为数据或表结构更新,而出现备份文件的数据与预期不一致
加全局锁带来的缺点
加上全局锁,意味着整个数据库都是只读状态
如果数据库有很多数据,备份需要花费很长时间,在备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞
既然备份数据库数据时,使用全局锁会影响业务,有其他方式可以避免吗?
有的,如果支持事务可重复读的隔离级别,在备份数据库前先开启事务,会创建 Read View,整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作
备份数据库工具是 mysqldump,在使用 mysqldump 时加上 –single-transaction
参数,就会在备份数据库之前先开启事务
这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎,如 InnoDB;但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法
表级锁
MySQL 中表级别的锁包括:
- 表锁
- 元数据锁(MDL)
- 意向锁
- AUTO-INC 锁
表锁
加锁:
# 表级别的共享锁,也就是读锁
lock tables t_student read;
# 表级别的独占锁,也就是写锁
lock tables t_stuent write;
注意:表锁除了会限制其他线程读写外,也会限制本线程接下来的读写操作。也就是说如果本线程对表加了「共享表锁」,那么本线程接下来对该表执行写操作,也是会被阻塞的
释放当前会话的所有表锁:
unlock tables
另外,当会话退出后,也会释放所有表锁
尽量避免在 InnoDB 的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 提供了颗粒度更细的行级锁
元数据锁(MDL)
不需要显式使用 MDL,因为当对数据库表进行操作时,会自动给这个表加上 MDL:
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁
- 对一张表做结构变更操作时,加的是 MDL 写锁
MDL 是为了保证 CRUD 操作和表结构变更发生冲突:
- 当有线程在执行 CRUD(加 MDL 读锁)期间,有其他线程要更改该表的结构(申请 MDL 写锁),会被阻塞,直到执行完 CRUD 操作(释放 MDL 读锁)
- 反之,当有线程对表结构进行变更(加 MDL 写锁)期间,有其他线程执行了 CRUD 操作(申请 MDL 读锁),会被阻塞,直到表结构变更完成(释放 MDL 写锁)
MDL 不需要显示调用,那它是在什么时候释放的?
MDL 在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的
如果数据库有一个长事务(开启了事务,但是一直没提交),那在对表结构做变更操作时可能出现问题,如:
- 线程 A 启用了事务(但是一直不提交),然后执行一条
select
语句,此时就对该表加上 MDL 读锁 - 线程 B 也执行了
select
语句,此时并不会阻塞,因为「读读」并不冲突 - 线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,会被阻塞
- 在线程 C 阻塞后,后续有对该表的
select
语句,都会被阻塞,如果此时有大量该表的select
语句请求到来,就会有大量的线程被阻塞住,数据库的线程很快就会爆满
为什么线程 C 因为申请不到 MDL 写锁,而导致后续的申请读锁的查询操作也会被阻塞?
这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作
所以为了能安全的对表结构进行变更,在变更前,先要看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,可以考虑 kill 掉这个长事务,然后再做表结构的变更
意向锁
意向锁是自动加锁和解锁的:
- 对 InnoDB 表中某些记录加「共享锁」前,需要先在表级别加上一个「意向共享锁」
- 对 InnoDB 表中某些记录加「独占锁」前,需要先在表级别加上一个「意向独占锁」
意向锁的作用:
- 意向锁是表级锁,不会和行级锁发生冲突,而且意向锁之间也不会发生冲突,只会和表锁发生冲突
- 如果没有意向锁,加表锁时,就需要遍历表里所有行,查看是否有行存在锁,这样效率会很低
- 有了意向锁,加表锁时,直接查该表是否有意向锁即可,无需遍历所有行
所以,意向锁的目的是为了快速判断表中是否有行被加锁
AUTO-INC 锁
表中声明 AUTO_INCREMENT
的列数据会自增
在插入数据时,会给表加 AUTO-INC 锁,为被 AUTO_INCREMENT
修饰的字段赋值递增的值,等插入语句执行完成后,释放 AUTO-INC 锁
一个事务在持有 AUTO-INC 锁的过程中,其他事务如果要向该表插入会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT
修饰的字段值是连续递增的
但是,在大量数据插入时,AUTO-INC 锁会影响插入性能,因此, 在 MySQL 5.1.22 开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增
一样也是在插入数据时,为被 AUTO_INCREMENT
修饰的字段加轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁
InnoDB 提供 innodb_autoinc_lock_mode
系统变量,用来控制选择用 AUTO-INC 锁,还是轻量级锁:
innodb_autoinc_lock_mode = 0
:采用 AUTO-INC 锁innodb_autoinc_lock_mode = 2
:采用轻量级锁innodb_autoinc_lock_mode = 1
:- 普通
insert
语句:采用轻量级锁,申请之后就马上释放 - 类似
insert ... select
这样的批量插入数据的语句:采用 AUTO-INC 锁,要等语句结束后才被释放
- 普通
当 innodb_autoinc_lock_mode = 2 是性能最高的方式,但是当搭配 binlog 的日志格式是 statement 一起使用的时候,在「主从复制的场景」中会发生数据不一致的问题。
行级锁
InnoDB 支持行级锁,MyISAM 不支持
MySQL 中行级别的锁包括:
- 记录锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock)
记录锁
记录锁(Record Lock):就是把一条记录(一行)锁上
同意向锁,记录锁也是自动加锁和解锁的
当执行插入、更新、删除操作,先对表加上「意向独占锁」,然后对该行加独占锁
而普通的 select
是不会加行级锁的(除了串行化隔离级别),因为它属于快照读,利用 MVCC 实现一致性读,是无锁的
不过,select
也是可以对行加共享锁和独占锁的(称为锁定读):
# 先在表上加意向共享锁,然后对读取的行加共享锁
select ... lock in share mode;
# 先在表上加意向独占锁,然后对读取的行加独占锁
select ... for update;
注意:上面的语句必须在事务中,因为当事务提交了,锁就会被释放
间隙锁
间隙锁(Gap Lock):锁定一个范围,不锁定记录本身。即:在范围内不可插入删除记录,但可修改记录的数据
间隙锁是前开后开区间
只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象(详见)
间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的
临键锁
临键锁(Next-Key Lock):记录锁 + 间隙锁,锁定一个范围,且锁定记录本身。即:在范围内不可插入删除记录,也不可修改记录的数据
临键锁是前开后闭区间
因为临键锁是记录锁 + 间隙锁,所以虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,是要考虑 X 型与 S 型关系
插入意向锁
一个事务在插入一条记录时,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)
如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态
MySQL 加锁时,是先生成锁结构,然后设置锁状态,如果锁状态是等待状态,并不意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁
插入意向锁名字虽然有意向锁,但是它并不是意向锁,而是一种特殊的间隙锁,属于行级别锁
如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点
插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁