• 读锁:S 锁、共享锁
  • 写锁:X 锁、独占锁

读读共享,写写互斥、读写互斥

即:

读锁写锁
读锁兼容互斥
写锁互斥互斥

全局锁(库级锁)

使用

加锁:

flush tables with read lock

执行后,整个数据库处于只读状态,这时写操作都会被阻塞:

  • 对数据的增删改操作,如 insertdeleteupdate
  • 对表结构的更改操作,如 alter tabledrop 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 是一直持有的

如果数据库有一个长事务(开启了事务,但是一直没提交),那在对表结构做变更操作时可能出现问题,如:

  1. 线程 A 启用了事务(但是一直不提交),然后执行一条 select 语句,此时就对该表加上 MDL 读锁
  2. 线程 B 也执行了 select 语句,此时并不会阻塞,因为「读读」并不冲突
  3. 线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,会被阻塞
  4. 在线程 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 加锁时,是先生成锁结构,然后设置锁状态,如果锁状态是等待状态,并不意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁

插入意向锁名字虽然有意向锁,但是它并不是意向锁,而是一种特殊的间隙锁,属于行级别锁

如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁