MySQL-锁

一、MySQL有哪些锁?

  在 MySQL 中,锁具有很重要的作用,它可以保证事务的隔离性,同时保证数据的完整性和有效性。根据加锁的范围,可以分为全局锁、表级锁和行锁三类。

1、全局锁

(1)特点

  加上全局锁后,整个数据库就处于只读状态了,这时其他线程对数据执行增删改,或者对表结构进行变更的操作,都会被阻塞,直到全局锁被释放。

(2)适用场景

  全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期不一样的问题。
  但是如果数据库里有很多数据,备份就会花费很多的时间,这期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。

避免业务停滞的方法
  InnoDB引擎支持事务,在可重复读的隔离级别中,备份数据库之前先开启事务,会创建一个 Read View,然后整个事务执行期间都在用这个 Read View,由于 MVCC的支持,备份期间业务依然可以对数据进行更新操作。
  但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。

(3)使用

要使用全局锁,可以执行如下命令:

flush tables with read lock

如果要释放全局锁,则要执行这条命令:

unlock tables

2、表级锁

  MySQL 中,表级别的锁有这几种:表锁,元数据锁(MDL),意向锁,AUTO-INC 锁。

(1)表锁

①特点
  • 读锁允许其他线程对该表的读操作,但是会阻止其他线程对该表的写操作,还会限制当前线程对该表的写操作和对其他表的读写操作。
  • 写锁会阻止其他线程对该表的读写操作,还会限制当前线程对其他表的读写操作。
②适用场景

  在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式,不过表锁的颗粒度还是太大,会影响并发性能,尽量避免在使用InnoDB引擎的表使用表锁,因为InnoDB 还实现了颗粒度更细的行级锁。

③使用

如果我们想对学生表(t student)加表锁,可以使用下面的命令

--表级别的共享锁,也就是读锁;
--允许当前会话读取被锁定的表,但阻止其他会话对这些表进行写操作。
lock tables t student read;

--表级别的独占锁,也就是写锁;
--允许当前会话对表进行读写操作,但阻止其他会话对这些表进行任何操作(读或写)。
lock tables t stuent write,

要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:

unlock tables

另外,当会话退出后,也会释放所有表锁。

(2)元数据锁(MDL)

①特点
  • 不需要显示使用,当我们对数据库表进行操作时,会自动给这个表加上 MDL。
  • 对一张表进行增删查改操作时,加的是 MDL 读锁,此时将会阻塞其他线程更改该表的结构的操作(申请 MDL写锁),直到执行完 select 语句 (释放 MDL 读锁)。
  • 对一张表做结构变更操作的时候,加的是 MDL 写锁,此时将会阻塞其他线程执行 CRUD 操作(申请MDL读锁),直到表结构变更完成(释放 MDL写锁)。
  • 在事务提交后才会释放。
②作用

  保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

(3)意向锁

①特点

当你在 MySQL 中请求独占锁或共享锁(行级锁)时,系统会自动加上相应的意向锁。

  • 在使用 InnoD8 引擎的表里对某些记录加上「共享锁」之前,会自动在表级别加上一个「意向共享锁」;
//先在表上加上意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode;
  • 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,会自动在表级别加上一个「意向独占锁」;
//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
  • 意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突;意向锁之间也不会发生冲突,只会和表锁的读锁和写锁发生冲突;
  • 表锁和行锁是满足读读共享、读写互斥、写写互斥的。
②作用

  如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。
  那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
  所以,意向锁的目的是为了快速判断表里是否有记录被加锁。

(4)AUTO-INC 锁

①特点
  • 在插入数据时,会加一个表级别的 AUTO-INC锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,就会把 AUTO-INC 锁释放掉,不会等事务提交。
  • 一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。
②作用
  • 在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值。

  AUTO-INC锁在对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此,在 MySQL 5.1.22 版本开始,InnoD8 存储引擎提供了一种轻量级的锁来实现自增,不需要等待整个插入语句执行完后才释放锁。
  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 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放。

  当 innodb_autoinc_lock_mode =2是性能最高的方式,但是当搭配 binlog 的日志格式是 statement(记录的是SQL) 一起使用的时候,在「主从复制的场景」中会发生数据不一致的问题。因为如果有两个线程在交叉插入数据,binlog会先记录完其中一个线程的所有SQL,再记录另一个线程的SQL,那么主库自增id对应的是两个线程交叉插入的数据,但是从库读取binlog后自增id对应的是两个线程前后分别插入的数据。但是如果 binlog_format = row(记录的是行的变更),既能提升并发性,又不会出现数据一致性问题。

3、行级锁

  行级锁分为共享锁和独占锁。共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。行级锁是系统为不同的SQL自动加上的,在事务提交后释放。

MySQL-锁_第1张图片

行级锁的类型主要有四类:

(1)Record Lock

  记录锁,也就是仅仅把一条记录锁上,有S锁和X锁的区分:

  • 当一个事务对一条记录加了S型记录锁后,其他事务也可以继续对该记录加S型记录锁,但是不可以对该记录加 X型记录锁;
  • 当一个事务对一条记录加了X型记录锁后,其他事务既不可以对该记录加S型记录锁,也不可以对该记录加x型记录锁。

MySQL-锁_第2张图片

(2)Gap Lock

  间隙锁,锁定一个范围,但是不包含记录本身。
  间隙锁虽然存在X型间隙锁和S型间隙锁,但是并没有什么区别,都是阻塞其他事务在此间隙内插入新记录,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。

MySQL-锁_第3张图片

(3)Next-Key Lock

  Record Lock+ Gap Lock 的组合,即包含记录锁和间隙锁,锁定一个范围,并且锁定记录本身。所以,next-key lock 即能保护该记录,又能阻止其他事务将新记录插入到被保护记录前面的间隙中。
  Next-Key Lock 区分S锁和X锁,S锁与S锁之间不冲突,S锁与X锁、X锁与X锁之间是冲突的。

MySQL-锁_第4张图片

(4)插入意向锁

  一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
  如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

什么SQL会加行级锁?

1、select

  普通的 select 语句是不会对记录加锁的(除了串行化隔离级别),因为它属于快照读,通过 MVCC(多版本并发控制)实现可重复读。
  但是,也可以手动为普通的select语句加上行级锁,可以使用下面这两个方式,这两种会加锁的查询语句称为锁定读:

//对读取的记录加共享锁(S型锁)
select ... lock in share mode;
//对读取的记录加独占锁(X型锁)
select ... for update;

上面这两条语句必须在一个事务中,因为只有事务提交了,锁才会被释放。

2、update 和 delete

  update 和 delete 都会加行级锁,且锁的类型都是独占锁(X型锁)。

//对操作的记录加独占锁(x型锁)
update table ... where id =1:
//对操作的记录加独占锁(x型锁)
delete from table where id = 1;

MySQL 是怎么加行级锁的?

  行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。
  加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间。
  但是,在能使用记录锁或者间隙锁就能避免幻读现象的场景下,next-key lock 就会退化成记录锁或间隙锁。

唯一索引等值查询
  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」;
  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的next-key lock 会退化成「间隙锁」。
非唯一索引等值查询
  • 当查询的记录「存在」时,由于不是唯一索引,所以可能存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。
  • 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。
唯一索引和非唯一索引的范围查询

非唯一索引和主键索引(也是唯一索引)的范围查询的加锁规则不同之处在于:

  • 唯一索引在满足一些条件的时候,索引的 next-key lock 退化为间隙锁或者记录锁。
  • 非唯一索引范围查询,索引的 next-key lock 不会退化为间隙锁和记录锁。

  其实要理解 MySQL为什么要这样加锁,可以从避免幻读的角度去分析,这样就很容易理解这些加锁的规则了。

没有加索引的查询

  前面的案例,都有使用索引查询,也就是查询记录的时候,是通过索引扫描的方式查询的,然后对扫描出来的记录进行加锁。
  如果锁定读,update 以及 delete 语句,没有走索引查询,就会导致全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时其他事务想对该表进行增、删、改操作的时候,都会被阻塞。
  因此,在线上在执行 update、delete、select… for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。

如何避免这种事故的发生?
(1)我们可以将 MySQL里的 sql_safe_updates参数设置为 1,开启安全更新模式。当 sql_safe_updates 设置为1时,

update 语句必须满足如下条件之一才能执行成功:

  • 使用 where,并且 where 条件中必须有索引列;
  • 使用 limit;
  • 同时使用 where 和 limit,此时 where 条件中可以没有索引列。

delete 语句必须满足以下条件能执行成功:

  • 同时使用 where 和 limit,此时 where 条件中可以没有索引列。

(2)如果 where 条件带上了索引列,但是优化器最终还是选择全表扫描,我们可以用force index([index_name])告诉优化器使用哪个索引,或者查看是否存在使索引失效的问题,以此减小锁全表的几率。

你可能感兴趣的:(mysql,数据库)