MySQL中的锁有哪些
导读:
在解决读写冲突产生的问题时,不同的数据库厂商使用的方式也不尽相同,但是大致可分为以下两种方案:
方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁。
对于读操作,一个查询语句会产生一个 Read View(读视图),在不同的隔离级别下,根据 Read View 找到该事务可以访问的历史版本数据;
对于写操作,通过对最新版本的数据进行加锁,只有获取到锁的事务才可以对数据进行修改,避免了脏写问题。
由此看出,读操作是针对历史版本数据、写操作则是针对最新版本数据,二者并不冲突,也就是采用MVCC时,读-写操作并不冲突。
方案二:读、写操作都采用加锁的方式。
如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本,比方在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行加锁操作,这样也就意味着读操作和写操作也像写-写操作那样排队执行。
很明显,采用 MVCC 方式的话,读-写操作彼此并不冲突,性能更高;采用加锁方式的话,读-写操作彼此需要排队执行,影响性能。一般情况下我们当然愿意采用 MVCC 来解决读-写操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁的方式执行,那也是没有办法的事。
所以,数据库中的锁还是很重要的一部分知识,接下来我们就开始关于锁的学习!
一、锁的分类
如果使用加锁的方式解决读写冲突产生的问题时,我们就得考虑既要允许读-读情况不受影响,又要使写-写、读-写情况中的操作相互阻塞,所以设计 MySQL 的大叔给锁进行了分类:
按锁的使用方式分:
- 共享锁(S锁):一种读锁,当一个事务对一条数据行加上了S锁,那么当前事务不能修改该行数据,只能执行读操作,其他事务也只能对该行数据加S锁而不能加X锁。
- 排它锁(X锁):一种写锁,当一个事务对一条数据行加上了X锁,那么当前事务能够对该行数据执行读和写操作,其他事务不能对该行数据加任何锁,在当前事务释放锁之前既不能读也不能写。
- 兼容性:
兼容性 | X | S |
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
按锁的粒度分:
- 全局锁
- 表级锁:粒度最大、加锁快、开销小、不会出现死锁,但并发性低。
- 行级锁:粒度最小、加锁慢、开销大、会出现死锁,但并发性高。
注:表级锁、行级锁都可以分为S锁、X锁。
二、全局锁:
1、加锁方式:执行 flush tables with read lock 命令
2、作用:执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:
- 对数据的增删改操作,比如 insert、delete、update等语句;
- 对表结构的更改操作,比如 alter table、drop table 等语句。
3、解锁方式:执行 unlock tables 命令
4、使用说明:如果未执行解锁命令,当会话断开后,全局锁也会被自动释放。
5、使用场景:全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。当然,这种备份库的方式缺点也很明显,如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。
三、表锁:
1、表级别的S锁、X锁
- 加锁方式:
- S锁:lock tables t_student read;
- X锁:lock tables t_stuent write;
- 兼容性说明:
- 给表加S锁:
如果一个事务给表加了S锁,那么:
- 别的事务可以继续获得该表的S锁
- 别的事务可以继续获得该表中的某些记录的S锁
- 别的事务不可以继续获得该表的X锁
- 别的事务不可以继续获得该表中的某些记录的X锁
- 给表加X锁:
如果一个事务给表加了X锁(意味着该事务要独占这个表),那么:
- 别的事务不可以继续获得该表的S锁
- 别的事务不可以继续获得该表中的某些记录的S锁
- 别的事务不可以继续获得该表的X锁
- 别的事务不可以继续获得该表中的某些记录的X锁
- 解锁方式:执行 unlock tables 或 当会话退出后,也会释放所有表锁。
2、元数据锁 Metadata Lock:
- 作用:用于保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
- 加锁时机:
我们不需要显式的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:
对一张表进行 CRUD 操作时,加的是 MDL 读锁;
对一张表做结构变更操作的时候,加的是 MDL 写锁。
- 加锁说明:
当有线程在执行 select 语句(加 MDL 读锁)的期间,如果有其他线程要更改该表的结构(申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句(释放 MDL 读锁)。
反之,当有线程对表结构进行变更(加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成(释放 MDL 写锁)。
- 解锁时机:MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。
3、意向锁 Intention Lock:
- 意向共享锁(IS锁):当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
- 意向独占锁(IX锁):当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
注:IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁也是兼容的。
4、AUTO-INC锁
- 作用:当表中主键字段声明为 AUTO_INCREMENT 属性后,因为AUTO-INC锁的存在,所以我们在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值。
- 加/解锁时机:
AUTO-INC锁也无需我们显式使用,而其加/解锁时机在不同版本时也不一样。
在 MySQL 5.1.22 版本之前,AUTO-INC锁被获得后,不再是一个事务提交之后才会被释放,而是执行完一条 insert 语句就会被立即释放。但即使如此,AUTO-INC锁在对大量数据进行插入的时候,也会影响插入性能,因为另一个事务中的插入会被阻塞。
从 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。同样的,还是在插入语句开始时,为被 AUTO_INCREMENT 修饰的字段加轻量级锁,但是不需要等待插入语句执行完成后再释放锁,而是主键自增的字段获取到递增的值后,就把该锁释放了。
- innodb_autoinc_lock_mode 的系统变量:
此外,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 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
四、行锁:
在RC、RR级别下,普通的select是基于 MVCC 的一种当前读,不会加行级锁;要想对普通select加锁需要对指定select语句进行显式化锁定读,或者串行化隔离级别下,普通select会被隐士转化为锁定读。
锁定读语句:
- S锁:SELECT ... LOCK IN SHARE MODE;
- X锁:SELECT ... FOR UPDATE;
1、记录锁 Record Lock:
仅仅把一条记录锁上,而且记录锁是有 S 锁和 X 锁之分的:
- 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
- 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
2、间隙锁 Gap Lock:
出现幻读时,因为这个幻影记录之前不存在,所以无法给其加Record Lock。但是可以对一个区间加Gap Lock,即锁定区间内不可以插入新记录。
如图中为number值为8的记录加了gap锁,意味着不允许别的事务在number值为8的记录前边的间隙插入新记录,其实就是number列的值(3, 8)这个区间的新记录是不允许立即插入的。
对于最后一条记录之后的间隙,也就是表中number值为20的记录之后的间隙,为了实现阻止其他事务插入number值在(20, +∞)这个区间的新记录,我们可以给索引中的最后一条记录,也就是number值为20的那条记录所在页面的Supremum记录加上一个gap锁,画个图就是这样:
间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。
3、临键锁 Next-Key Lock:
既能锁住某条记录,又能阻止其他事务在该记录前边的间隙插入新记录,本质就是一个Record Lock 和一个 Gap Lock 的合体。
临键锁也分为S锁、X锁,其兼容性如下:
请求锁 \ 已存在锁 | Next-Key S-Lock | Next-Key X-Lock |
Next-Key S-Lock | ✅ 兼容 | ❌ 不兼容 |
Next-Key X-Lock | ❌ 不兼容 | ❌ 不兼容 |
4、插入意向锁 Insert Intention Lock:
我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁(next-key锁也包含gap锁,后边就不强调了),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是设计InnoDB的大叔规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。设计InnoDB的大叔就把这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们也可以称为插入意向锁。
5、锁兼容性完整示意图
| S-Gap | X-Gap | S-Next-Key | X-Next-Key
--------|-------|-------|------------|-----------
S-Gap | ✅ | ❌ | ✅ | ❌
X-Gap | ❌ | ❌ | ❌ | ❌
S-Next | ✅ | ❌ | ✅ | ❌
X-Next | ❌ | ❌ | ❌ | ❌