MySQL 锁
- 全局锁
- 全局锁的应用场景
- 全局锁的缺点
- 表级锁
- 表锁
- 元数据(MDL)锁
- MDL 锁的问题
- 意向锁
- AUTO-INC 锁
- 行级锁
- 记录锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock)
- 插入意向锁
首先让我们创建一张用户表,并插入一些数据:
CREATE TABLE userinfo (user_id INT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) NOT NULL,password VARCHAR(50) NOT NULL,email VARCHAR(100),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);INSERT INTO userinfo (username, password, email) VALUES ('张三', 'password123', 'zhangsan@example.com');
INSERT INTO userinfo (username, password, email) VALUES ('李四', 'password456', 'lisi@example.com');
INSERT INTO userinfo (username, password, email) VALUES ('王五', 'password789', 'wangwu@example.com');
全局锁
插入全局锁的命令
flush tables with read lock
执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:
- 对数据的增删改操作,比如 insert、delete、update等语句;
- 对表结构的更改操作,比如 alter table、drop table 等语句。
比如此时我们想插入一条语句,但显示被读锁阻塞住了
执行释放锁之后的命令就可以插入成功了
unlock tables;
全局锁的应用场景
全局锁的应用场景主要应用于 全库逻辑备份
举个例子,如果不加读写锁的话
在备份用户表和商品表的时候,有一个用户购买了一件商品
就会导致备份的数据 是扣减前的用户余额 和 扣减后的商品余额 —— 用户余额没变,货却没了!
全局锁的缺点
加上全局锁后,数据库就变成了 只读状态
那么如果数据库有很多数据,备份就会花费很多时间,而且备份期间只能读数据而不能更新数据,就会导致业务的停滞
那有什么办法可以改进呢?
可以利用 MVCC 机制,在可重复读隔离级别下,开启事务后,会先创建一个 Read View,然后整个事务执行期间都会使用这个 Read View,同时也依然可以对数据进行更新操作。
这就是事务四大特性的 隔离性,这样备份期间备份的数据一直都是在开启事务时的数据。
注意,这需要能够适用 可重复读隔离级别 下的存储引擎,比如 Innodb。
但是 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法了。
表级锁
表锁
表锁有两种:
//表级别的共享锁,也就是读锁;
lock tables t_student read;//表级别的独占锁,也就是写锁;
lock tables t_stuent write;
这里的锁有点乱,我们可以做四次实验试试看
- 给表设置读锁,然后读数据,发现是正常没问题的
- 然后我们进行写数据,发现被阻塞住了
如图,这里我们给用户表设置了读锁,然后想写入一条数据,发现被阻塞住了
说明这里的本线程的表级别的读锁,会限制住本线程的写操作
- 我们解开表级锁,并设置读锁,然后再进行读操作
发现这里,阻塞住了,陷入了死锁状态,注意右下角的查询时间,这里是一直在查询没停止的(我只是截了个图),说明本线程的写锁也会限制住本线程的读操作。
- 我们在写锁下进行写操作,正常没问题
所以,可以看出来,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
所以,表锁的颗粒度是很大的,会影响到并发性能,尽量要避免使用,Innodb 牛逼的地方在于实现了颗粒度更细的行级锁。
元数据(MDL)锁
我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:
- 当我们对一张表进行 CRUD 操作时,会对表加一个 元数据读锁;
- 当我们对一张表的结构进行操作的时候,会对表加一个 元数据写锁
MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
反之,当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
MDL 锁的问题
MDL 是在事务提交之后才会释放的,也就是说,事务执行期间,MDL 是一直持有的。
那如果数据库中有一个长事务,也就是开启之后一直没提交的事务,那当我们对表结构做变更操作的时候,可能就会出现一些问题:
- 线程A 开启了事务
- 线程A 执行了一条查询 select 语句,这时会加 MDL读锁
- 线程B 执行了一条查询 select 语句,这时不会阻塞,因为读读并不冲突
- 线程C 试图修改表字段,但此时事务A 并没有提交,所以 MDL 读锁就还占用着,此时线程C 就无法申请到 MDL 写锁,而导致阻塞
- 之后的线程 D、E、F 想要执行查询语句时,就会被阻塞住,然后数据库的线程就会爆满
为什么写锁阻塞住,读锁也会被阻塞住?
因为 申请 MDL 锁的操作会形成一个队列,队列中 写锁获取的优先级高于读锁,一旦出现MDL 写锁等待,就会阻塞住该表的所有 CRUD 操作
所以,为了能安全的对表结构进行变更,在变更之前,我们要先看看数据库中的长事务,是否有事务已经对表加上了 MDL读锁,如果可以就考虑先 kill 掉他,然后再做变更。
意向锁
意向锁有两种:意向共享锁、意向排他锁:
- 意向共享锁(Intention Shared Lock,IS):当一个事务请求获取一个行级锁或表级锁时,InnoDB会自动获取相应的表的意向锁。
这个锁表明事务打算以共享方式(读取)访问数据。其他事务在查询数据时,可以通过检查意向锁来快速确定是否有任何行级锁冲突,而无需检查表中每一行的锁状态。
- 意向排他锁(Intention Exclusive Lock,IX):与意向共享锁类似,意向排他锁表明事务打算以排他方式(写入)访问数据。
当事务持有意向排他锁时,其他事务不能获取意向共享锁,从而防止其他读取操作的干扰。
也就是说,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。
而普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。
不过,select 也是可以对记录加共享锁和独占锁的,具体方式如下:
//先在表上加上意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode; //先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。
表锁和行锁是满足读读共享、读写互斥、写写互斥的。
如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。
那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
所以,意向锁的目的是为了快速判断表里是否有记录被加锁。
AUTO-INC 锁
auto-inc 锁(AUTO-INCREMENT lock),是InnoDB中用于处理自动增长(AUTO_INCREMENT)字段的一种特殊的锁定机制。当向表中插入数据时,如果该表包含有AUTO_INCREMENT列,并且你需要为这个列指定一个值,那么会涉及到auto-inc锁。
auto-inc锁的主要目的是确保在同一事务中插入的记录能够获得连续的AUTO_INCREMENT值。在并发插入的场景中,这可以避免插入操作之间的干扰,确保插入的顺序性。
行级锁
InnoDB 是支持行级锁的,而 MyISAM 是不支持的
记录锁(Record Lock)
记录锁是行级锁的一种,它针对数据表中的单个记录进行锁定,有 S 锁 和 X 锁之分,也就是共享锁和独占锁之分。
一个事务对一条记录加了 S 锁,其他事务可以加 S 锁,但不能加 X 锁
一个事务对一条记录加了 X 锁,其他事务 S 锁和 X 锁都不能加
记录锁通常是通过索引项来实现的,即数据库会锁定索引项来实现对行的锁定。
间隙锁(Gap Lock)
Gap Lock是锁定数据表中两个记录之间的空隙(即间隙锁)。
间隙锁只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的问题。
它用于防止其他事务插入新的记录,从而保护一个范围行的完整性。
Gap Lock不会锁定实际的行记录,而是锁定一个范围,阻止其他事务在这个范围内插入新行。
而且,间隙锁之间是不冲突的,可以相互兼容,只不过无法往间隙范围内插入数据
临键锁(Next-Key Lock)
Next-Key Lock是 记录锁 和 间隙锁 的组合。
它技能保护锁住的该条记录,又能阻止其他事务将其新纪录插入到被保护记录前面的间隙中
因为临键锁是包含 记录锁 + 间隙锁的,而间隙锁之间是可以相互兼容的,但记录锁就需要注意考虑 S 型 和 X 型的关系了
插入意向锁
插入意向锁是与上面的间隙锁对应生效的
如果一条记录的位置已经被其他事务加了间隙锁(当然,也包含临键锁中的间隙锁),那么,这个插入操作就会被阻塞,在此期间会生成一个 插入意向锁,表明有事务想要在某个区间插入新纪录,但是现在处于等待状态
比如事务 A 在一个区间内加了间隙锁,然后事务 B 想要在该区间插入一条语句,这时候发现有个间隙锁,于是就会生成一个 插入意向锁,然后将锁呢设置为等待状态。
MySQL 加锁的时候,会先生成锁结构,然后设置锁状态,锁状态为正常状态,就表示事务获取到了锁,如果为等待状态,就意味着没有获取到锁