电子说
前面提到的S
锁和X
锁的语法规则其实是针对记录的,也就是行锁,原因是InnoDB中行锁用的最多。如果将锁的粒度和锁的基本模式排列组合一下,就会出现如下4种情况:
S
锁X
锁S
锁X
锁那么接下来的描述,也就顺理成章了。
如果事务给一个表添加了表级S
锁,则:
S
锁,但是无法获取该表的X
锁;S
锁,但是无法获取该表某些行的X
锁。如果事务给一个表添加了表级X
锁,则:
S
锁、X
锁,还是该表某些行的S
锁、X
锁,其他事务都只能干瞪眼儿,啥也获取不了。挺好理解的吧,总之就是 S锁只能和S锁相容,X锁和其他任何锁都互斥 。问题来了,虽然用的不多,但是万一我真的想给整个表添加一个S
锁或者X
锁怎么办?
假如我要给表user
添加一个S
锁,那就必须保证user
在表级别上和行级别上都不能有X
锁,表级别上还好说一点,无非就是1个内存结构罢了,但是行X
锁呢?必须得逐行遍历是否有行X
锁吗?
同理,假如我要给表user
添加一个X
锁,那就必须保证user
在表级别上和行级别上都不能有任何锁(S
和X
都不能有),难不成得逐行遍历是否有S
或X
锁吗?
遍历是不可能遍历的!这辈子都不可能遍历的!于是, 意向锁 (Intension Lock)诞生了。
我们要避免遍历,那最好的办法就是在给行加锁时,先在表级别上添加一个标识。
IS
锁,当事务试图给行添加S
锁时,需要先在表级别上添加一个IS
锁;IX
锁,当事务试图给行添加X
锁时,需要先在表级别上添加一个IX
锁。这样一来:
user
表添加一个S
锁(表级锁),就先看一下user
表有没有IX
锁;如果有,就说明user
表的某些行被加了X
锁(行锁),需要等到行的X
锁释放,随即IX
锁被释放,才可以在user
表中添加S
锁;user
表添加一个X
锁(表级锁),就先看一下user
有没有IS
锁或IX
锁;如果有,就说明user
表的某些行被加了S
锁或X
锁(行锁),需要等到所有行锁被释放,随即IS
锁或IX
锁被释放,才可以在user
表中添加X
锁。需要注意的是,意向锁和意向锁之间是不冲突的,意向锁和行锁之间也不冲突。
只有在对表添加
S
锁或X
锁时才需要判断当前表是否被添加了IS
锁或IX
锁,当为表添加IS
锁或IX
锁时,不需要关心当前表是否已经被添加了其他IS
锁或IX
锁。
目前为止MySQL锁的基本模式就介绍完了,接下来回到这片文章的题目,MySQL锁,锁住的到底是什么?由于InnoDB的行锁用的最多,这里的锁自然指的是行锁。
既然都叫行锁了,我们姑且猜测一下,行锁锁住的是一行数据。我们做个实验。
我们先创建一张没有任何索引的普通表,语句如下
CREATE TABLE `user_t1` (
`id` int DEFAULT NULL,
`name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表中数据如下:
mysql> SELECT * FROM user_t1;
+------+-------------+
| id | name |
+------+-------------+
| 1 | chanmufeng |
| 2 | wanggangdan |
| 3 | wangshangju |
| 4 | zhaotiechui |
+------+-------------+
接下来我们在两个session中开启两个事务。
WHERE id = 1
“锁住”第1行数据;WHERE id = 2
"锁住"第2行数据。一件诡异的事情是,第2个加锁的操作被阻塞了。实际上,T2
中不管我们要给user_t1
中哪行数据加锁,都会失败!
为什么我SELECT
一条数据,却给我锁住了整个表?这个实验直接推翻了我们的猜测, InnoDB的行锁并非直接锁定Record行 。
为什么没有索引的情况下,给某条语句加锁会锁住整个表呢?别急,我们继续。
我们再创建一个表user_t2
,语句如下:
CREATE TABLE `user_t2` (
`id` int NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
和user_t1
的不同之处在于为id
创建了一个主键索引。表中数据依然如下:
mysql> SELECT * FROM user_t2;
+------+-------------+
| id | name |
+------+-------------+
| 1 | chanmufeng |
| 2 | wanggangdan |
| 3 | wangshangju |
| 4 | zhaotiechui |
+------+-------------+
同样开启两个事务:
WHERE id = 1
“锁住”第1行数据;WHERE id = 1
尝试加锁,加锁失败;WHERE id = 2
尝试加锁,加锁成功。既然锁的不是Record行,难不成锁的是id
这一列吗?
我们再做最后一个实验。
我们再创建一个表user_t3
,语句如下:
CREATE TABLE `user_t3` (
`id` int NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`uk_name`) (`name`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
和user_t2
的不同之处在于为name
列创建了一个唯一索引。表中数据依然如下:
mysql> SELECT * FROM user_t3;
+------+-------------+
| id | name |
+------+-------------+
| 1 | chanmufeng |
| 2 | wanggangdan |
| 3 | wangshangju |
| 4 | zhaotiechui |
+------+-------------+
两个事务:
name
字段 “锁住”name
为“chanmufeng”的数据;WHERE name = “chanmufeng”
尝试加锁,可以预料,加锁失败;WHERE id = 1
尝试给同样的行加锁,加锁失败。通过3个实验我们发现,行锁锁住的既不是Record行,也不是Column列,那到底锁住的是什么?我们对比一下,上文的3张表的不同点在于索引不同,其实 InnoDB的行锁,就是通过锁住索引来实现的 。
接下来回答3个问题。
你说锁住索引?如果我不创建索引,MySQL锁定个啥?
如果我们没有设置主键,InnoDB会优先选取一个不包含NULL值的Unique键
作为主键,如果表中连Unique键
也没有的话,就会自动为每一条记录添加一个叫做DB_ROW_ID
的列作为默认主键,只不过这个主键我们看不到罢了。
下图是数据的行格式。看不懂的话强烈推荐看一下我上面给出的两篇文章,说得非常明白。
行格式
因为SELECT
没有用到索引,会进行全表扫描,然后把DB_ROW_ID
作为默认主键的聚簇索引都给锁住了。
不管是Unique
索引还是普通索引,它们的叶子结点中存储的数据都不完整,其中只是存储了作为索引并且排序好的列数据以及对应的主键值。
因此我们通过索引查找数据数据实际上是在索引的B+树中先找到对应的主键,然后根据主键再去主键索引的B+树的叶子结点中找到完整数据,最后返回。所以虽然是两个索引树,但实际上是同一行数据,必须全部锁住。
下面给了一张图,让不了解索引的朋友大致了解一下。上半部分是name
列创建的唯一索引的B+树,下半部分是主键索引(也叫聚簇索引)。
假如我们通过WHERE name = '王钢蛋'
对数据进行查询,会先用到name
列的唯一索引,最终定位到主键值为1
,然后再到主键索引中查询id = 1
的数据,最终拿到完整的行数据。
这两张图在我索引文章中都有哦~
MySQL锁-索引
至此,我已经回答了文章开头的绝大多数问题。
MySQL锁,是解决资源竞争问题的一种手段。有哪些竞争呢?读—写/写—读,写—写中都会出现资源竞争问题,不同的是前者可以通过MVCC的方式来解决,但是某些情况下你也不得不用锁,因此我也顺便解释了锁和MVCC的关系。
然后介绍了MySQL锁的基本模式,包括共享锁(S
锁)和排他锁(X
锁),还引入了意向锁。
最后解释了锁到底锁的是什么的问题。通过3个实验,最终解释了InnoDB锁本质上锁的是索引。
全部0条评论
快来发表一下你的评论吧 !