MySQL的锁
读锁和写锁
读锁还可以称为共享锁 ,写锁还可以称为排他锁
读锁和写锁是系统层面上的锁,也是最基础的锁。
读锁和写锁加锁关系如下:
读锁 | 写锁 | |
---|---|---|
读锁 | 共存 | 互斥 |
写锁 | 互斥 | 互斥 |
一个请求占用了读锁,其他请求也可以过来加读锁,但是不能加写锁。这种情况下会出现一个问题,如果一直有请求过来加读锁,那么来了一个加写锁的请求,会一直因为有读锁的存在而阻塞。这样写锁就会被饿死了,为了避免这种情况的发生,数据库做了优化,当有写锁阻塞时,后面的读锁也会阻塞,这样就避免了写锁饿死的现象。
对于 InnoDB 引擎而言,采用的是B+树索引,假设需要将整个表锁住 ,那么需要在整个B+树的每个节点上都加锁,这种方式显然是很低效的。
因此MySQL提出了意向锁的概念,如果要在一个节点上加锁,就必须要在其所有的祖先节点上加意向锁。
表锁和行锁
表锁和行锁是两种不同粒度的锁,除了这两种锁还有一个更大粒度的锁:全局锁
-
全局锁
-
全局锁会锁住整个数据库;
-
MySQL使用
flush tables with read lock
命令来加全局锁,使用unlock tables
解锁; -
当加上全局锁以后,除了当前线程以外,其他线程的更新操作都会被阻塞,包括增删改数据表中的数据、建表、修改表结构等;
-
全局锁的典型使用场景是全库的逻辑备份。
-
-
表锁
- 表锁会锁住一张表;
- MySQL 使用
lock tables read/write
命令给表加上读锁或写锁,通过unlock tables
命令释放表锁- 通过
lock tables t read
给表 t 加上读锁后,当前线程只能访问表 t,不能访问数据库中的其他表,对表 t 也只有读权限,不能进行修改操作。 - 通过
lock tables t write
给表 t 加上写锁后,当前线程只能访问表 t,不能访问数据库中的其他表,对表 t 有读写权限。
- 通过
-
行锁
-
行锁会锁锁住表中的某一行或者多行
-
MySQL 使用
lock in share mode
命令给行加读锁,用for update
命令给行加写锁,行锁不需要显示释放,当事务被提交时,该事务中加的行锁就会被释放。通过
select k from t where k = 1 for update
命令可以锁住 k 为 1 的所有行 -
当使用 update 命令更新表数据时,会自动给命中的行加上行锁
-
MySQL 加行锁时并不是一次性把所有的行都加上锁,执行一个 update 命令之后,server 层将命令发送给 InnoDB 引擎,InnoDB 引擎找到第一条满足条件的数据,并加锁后返回给 server 层,server 层更新这条数据然后传给 InnoDB 引擎。完成这条数据的更新后,server 层再取下一条数据
-
事务A | 事务B | 事务C | 说明 |
---|---|---|---|
begin | 事务A开启事务 | ||
select * from t where id = 3 for update; | 事务A通过 for update将id=3的行锁住 | ||
update t set c = 0 where id = c; | 事务B执行update命令(将所有的c都更新为0)(执行到id=3的行时会阻塞) | ||
set session transaction isolation level READ UNCOMMITTED; | 开启事务 C,并且将事务 C 的隔离级别修改为未提交读; | ||
select * from t; | 事务C查询表信息,发现前两行的c都被更新为0,但是id=3的c并没有被事务B修改为0,说明事务B被阻塞了 | ||
commit | 提交事务A(事务A加的锁释放了,事务B可以继续加锁了,继续更新) | ||
select * from t; | 查询表的信息,发现都事务B已经更新完成 |
(可以看到事务B执行到id=3的行就被阻塞了,后面的数据都没有更新)
(太久没操作搞超时了,,,又重新按流程来了一遍,通过观察事务C的查询结果,可以发现事务A提交后事务B才继续执行了)