Files
rikako-note/mysql/mysql文档/锁.md
2024-11-16 18:29:21 +08:00

281 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 锁
## lock 和 latch
在mysql数据库中lock和latch都可以被称之为锁但是两者锁包含意义不同。
### latch
latch一般为轻量级的锁要求锁定时间通常非常短否则会影响性能。在innodb中latch可以分为mutex和rwlock。
`latch通常被用于防止代码中临界资源的并发访问`保证并发操作的安全性。latch操作通常没有死锁检测机制。
### lock
lock和latch则稍有区别lock一般针对的是事务用于锁定数据库中的表、页、行等数据。并且`lock一般在数据库事务进行commit或rollback后才释放`
在innodb中lock拥有死锁检测机制。
latch和lock的比较如下
| | lock | latch |
| :-: | :-: | :-: |
| 对象 | 事务 | 线程 |
| 保护 | 数据库内容 | 代码中的临界资源 |
| 持续时间 | 整个事务过程 | 临界资源 |
| 模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
| 死锁 | 通过waits-for graph、timeout等机制检测死锁 | 无死锁检测机制 |
在innodb中可以通过`show engine innodb status`以及`information_schema`下的`innodb_trx`来查看锁的情况。
## innodb中的锁
innodb中针对行级锁有如下两种实现
- S lock(共享锁):允许事务针对一行数据进行读取
- X lock(排他锁):允许事务针对一行数据进行更新和删除
S lock和S lock之间可以互相兼容但是S lock和X lock、 X lock和 X lock之间都是不兼容的。当一个事务t持有某行数据的X lock时其他事务必须等待事务t提交或回滚之后才能获取该行数据的S lock或X lock。
除了行锁之外innodb还支持多粒度的锁定允许行级和表级的锁同时存在。为了支持该操作innodb引入了一种额外的加锁方式`意向锁`
### 意向锁
意向锁支持多粒度加锁,允许行锁和表锁共存。例如,`lock tables ... write`会在指定表上加X锁。为了在多个粒度上进行加锁innodb使用了意向锁意向锁是表级锁代表事务稍后需要为表中的数据加上哪种锁。
意向锁分为两种类型:
- IS: 事务旨在为table中独立的行添加共享锁
- IX: 事务旨在为table中的行添加排他锁
例如,`select ... for share`设置IS锁`select ... for update`则设置了IX锁。
意向锁协议如下:
- 在事务获取table中某行记录的s锁之前其必须获取table的IS或更高级别锁
- 在事务获取table中某行记录的x锁之前其必须获取table的IX锁
table level锁的兼容性如下所示
| | x | ix | s | is |
| :-: | :-: | :-: | :-: | :-: |
| x | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
| ix | 不兼容 | 兼容 | 不兼容 | 兼容 |
| s | 不兼容 | 不兼容 | 兼容 | 兼容 |
| is | 不兼容 | 兼容 | 兼容 | 兼容 |
如果要加的锁和已经存在的锁兼容,那么它将被授予请求加锁的事务;要加的锁和已经存在的锁冲突时,则不会授予。请求加锁的事务将会阻塞,一直等待到存在冲突的锁被释放。
如果事务中的锁请求因为与现有锁冲突而无法被授予,并且造成死锁,那么会抛出异常。
> 意向锁和意向锁之间是兼容的,故而意向锁只会和全表扫描相冲突。
引入意向锁的意图是展示有人锁定了表中的某一行,或是将要锁定表中的某一行。
事务和意向锁相关的信息可以通过`show engine innodb status`来展示,其输出如下:
```
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
```
### Record Lock
记录锁是针对一条index record的锁。例如`SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE`语句会阻塞任何其他事务针对`c1 = 10`记录的插入、更新或删除。
records总是会针对index record加锁即使当前table没有定义索引。对于table未定义索引的情况innodb会创建一个隐藏的聚簇索引并使用该聚簇索引来进行record lock。
事务相关record lock的信息可以通过`SHOW ENGINE INNODB STATUS`来展示,其输出格式如下:
```
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
```
### 一致性读
#### 一致性非锁定读
`一致性非锁定读`是指innodb通过mvcc多版本并发控制来读取行数据。如果当前待读取的行正在执行update或delete操作那么此时`读取行的操作并不会被阻塞,而是会去读取当前被修改行的快照历史版本`。这种行为被成为`非锁定读`。
在非锁定读的场景下事务读取行数据时并不需要等待其他事务持有的X锁释放而是之前读取行数据的历史快照版本。`历史快照版本通过`undo log`来生成`。
非锁定读极大的提高了数据库的并发性一个事务对行数据的写操作并不会阻塞其他的事务对该行进行读取。innodb在默认的隔离级别下默认通过非锁定读来读取数据。
`在不同事务隔离级别下,读取数据的方式可能会不同,并非所有隔离级别都采用非锁定读的读取方式`。
> #### 多版本并发控制mvcc
> 在非锁定读场景下,一行数据通常不会只有一个历史版本,其数据快照并不只有一个,这被称为多版本并发控制。
>
> innodb对read committed和repeatable read隔离级别使用非锁定读但是两种隔离级别`快照定义并不相同`。
>
> #### read committed
>
> 在read committed隔离级别下非锁定读总是会读取行数据`最新的快照`。
>
> #### repeatable read
> 在repeatable read隔离级别下非锁定读则是会读取`事务开始时的行数据版本`。
#### 一致性锁定读
在默认隔离级别下read committed和repeatable read都是用非锁定读但是可以通过语法显式的支持锁定读。`用户可以通过加锁来保证读取数据的一致性`。
在通过select语句对数据加锁时支持加两种类型的锁
- select ... for update
- select ... lock in share mode
`select ... for update`实际是对读取的行记录加上X锁其他事务均不能对该数据添加任何的锁。
`select ... lock in share mode`则是对读取的记录加上S锁其他事务可以向被锁定记录加S锁但是不能加X锁。
`对于非锁定读即使数据被添加了X锁也可以进行读取。`只有通过`for update`或`lock in share mode`在读取时添加X锁或S锁时读取操作才会被阻塞。
并且,`for update`或`lock in share mode`添加的行锁在事务commit或rollback时会被释放。
### Gap Lock
gap lock是针对index record之间的间隙来进行加锁的或是针对第一条index record之前或最后一条index record之后的间隙进行加锁。例如`SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; `语句会阻止其他事务在c1值的`10~20`范围之间插入数据,不管这条数据存不存在,在`10~20`的间隙之前所有的值都被锁定了。
间隙范围可能跨单个索引值,多个索引值甚至为空。
间隙锁在部分的事务隔离级别中有使用,在其他隔离级别中则不会被使用。
在通过unique index区查询唯一行时并不需要使用间隙锁并不包含unique复合索引的情况。例如在id为唯一索引的情况下如下所示的语句只会使用index record lock
```sql
SELECT * FROM child WHERE id = 100;
```
但是如果id并不是索引或者id的缩影并不unique那么上述语句会对之前的间隙进行加锁。
值得注意的是不同事务针对同一间隙可以持有相互冲突的锁。例如A事务针对间隙持有S锁而B事务针对间隙持有X锁当index record从索引中被清除时不同事务所持有的间隙锁将会被合并。
在innodb中间隙锁只是为了防止其他事务在该间隙中插入锁间隙锁是可以共存的。如果事务A针对间隙加锁这并不会阻止事务B获取同一间隙的锁。`间隙的S锁和X锁都遵循该规则,且S锁和X锁之间并不会相互冲突间隙的S锁和X锁功能相同`。
间隙锁可以被显示的禁用,如果将事务的隔离级别改为`read committed`在查找和索引扫描时间隙锁将会被禁用间隙锁只会被用于外键检查和重复key检查。
### 行锁算法
innodb中有三种行锁的算法如下
- Record Lock单个行记录上的锁
- Gap Lock间隙锁锁定一个范围但是不包含记录本身
- Next-Key LockGap Lock + Record Lock锁定一个范围并且锁定记录本身
Record Lock是针对索引加的锁`如果在建表时没有为表设置索引那么innodb会采用隐式的主键来进行锁定。`
#### Next-Key Lock
Next-Key Lock结合了record lock和gap lock如果一个索引中有10111320这四条记录那么next-key lock的可锁定区间如下
- (-∞, 10]
- (10, 11]
- (11, 13]
- (13, 20]
- (20, +∞)
Next-Key Lock是为了解决幻读问题而引入的如果事务T1已经锁定了`(10, 11]`和`(11, 13]`区间那么在T1插入值为12的记录时锁定的范围会变为
`(10, 11] (11, 12] (12, 13]`。
但是,`如果查询的索引为unique索引那么innodb则是会针对next-key lock进行优化将其降级为record lock紧锁住索引本身而不对范围进行加锁。`
#### insert-intention lock
插入意向锁是在插入行数据前,由`插入操作`设置的间隙锁。多个事务在针对同一间隙进行插入操作时,`如果他们并不在同一位置进行插入,那么各个事务之间并不需要彼此等待`。
例如,多个事务需要在`(4, 7)`的间隙之间插入值但是A事务插入5B事务插入6那么在获取待插入行的X锁之前都需要通过insert intention lock来锁住`(4, 7)`的间隙。插入意向锁之间并不会相互阻塞。
### 加锁示例
首先,创建数据库并预制数据
```sql
-- ddl
CREATE TABLE `t_learn_lock` (
`id` bigint NOT NULL AUTO_INCREMENT,
`content` varchar(32) NOT NULL DEFAULT '',
`lv` int NOT NULL DEFAULT '-1',
PRIMARY KEY (`id`),
KEY `idx_lv` (`lv`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
-- dml
insert into t_lear_lock(id, content, lv) values
(1, 'a', 3),
(2, 'd', 7),
(3, 'f', 9),
(4, 'o', 13);
```
会话的事务默认隔离级别为repeatable read
#### performance_schema.data_locks
`performace_schema.data_locks`中存储了加锁情况,其表结构如下:
- engine该锁代表的存储引擎
- engine_transaction_id: 存储引擎中标识事务的id
- thread_id: `创建该锁的会话`的线程id
- object_schema: 包含该表的schema
- object_name表名称
- index_name: 被加锁的索引名称
- lock_type: 加锁的类型对于innodb其值为record或table代表行锁或表锁
- lock_mode: 请求锁的方式对于innodb其格式为`S[,GAP], X[,GAP], IS[,GAP], IX[,GAP], AUTO_INC, and UNKNOWN`
- lock_data:锁相关的数据
- lock_status: 请求锁的状态在innodb中可以为`GRANTED或WAITING`
> #### lock mode
> 在lock mode字段中除了auto_inc和unknown之外的值都代表gap lock
>
> 其取值如下:
> - X next_key_locking`代表x和x之前的间隙`
> - X,GAP间隙锁`代表x之前的间隙不包含x记录本身`
> - X, REC_NOT_GAP`代表X记录本身不包含之前的间隙`
> - X,GAP,INSERT_INTENTION插入意向锁彼此之前不互斥
#### select ... 加锁情况
在开启事务后,执行`select * from t_lear_lock`语句,默认`并不会为index record或表加上任何锁`,在可重复读的隔离级别下,默认会采用无锁的一致性读方式,来读取数据的历史版本快照,期间并不需要进行任何加锁操作。
#### select ... for update 加锁情况
在开启事务后,如果执行`select ... for update`语句,那么分为如下几种场景:
##### select语句未命中索引
若执行`select * from t_learn_lock where content < 'h' and content > 'e' for update for update;`语句由于content字段上并没有添加索引故而该查询语句并不会命中索引possible_keys为空。
在未命中索引的情况下,`select ... for update`语句`会针对主键索引来进行加锁`,其加锁情况如下:
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|----------|---------|---------|-----------|---------|
||TABLE|IX|GRANTED||
|PRIMARY|RECORD|X|GRANTED|supremum pseudo-record|
|PRIMARY|RECORD|X|GRANTED|1|
|PRIMARY|RECORD|X|GRANTED|2|
|PRIMARY|RECORD|X|GRANTED|3|
|PRIMARY|RECORD|X|GRANTED|4|
其中,`select * from t_learn_lock where content < 'h' and content > 'e' for update for update;`会查询出id为`3`的记录,但是由于未命中索引,`该语句会针对主键中所有的index record进行加锁`(包括 supremum prseudo-record)。
> ##### supremum preseudo-record
> 该index record并不是一条真实存在的索引记录其代表了`比索引中所有记录的值都大的一条虚拟记录`,类似有序链表中的尾节点。
##### select语句命中非unique索引
当执行的select语句其where条件命中非unique索引`那么,其会针对命中索引以及主键索引都进行加锁`
例如,`select * from t_learn_lock where lv between 4 and 8 for update;`,该语句会针对`idx_lv`索引记录和主键索引记录进行加锁,加锁状况如下:
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|----------|---------|---------|-----------|---------|
||TABLE|IX|GRANTED||
|idx_lv|RECORD|X|GRANTED|7, 2|
|idx_lv|RECORD|X|GRANTED|9, 3|
|PRIMARY|RECORD|X,REC_NOT_GAP|GRANTED|2|
上述查询语句会命中`(2, 'd', 7)`这条记录,但是,其针对索引加锁的操作如下:
- 对于`idx_lv`索引记录,不仅对语句查询出的`(7,2)`这条索引记录进行了加锁,还针对了下一条索引记录`(9,3)`进行了加锁
- 对于`primary`主键索引记录则只针对查询出的id为2的记录进行了加锁`(2)`
对于`idx_lv``(9,3)`这条记录进行加锁,主要是为了防止幻读,当对`(9, 3)`这条记录加锁后如果后续其他事务想要向lv值为`(7,9)`的范围内进行加锁时,需要获取
#### update ... 加锁情况
##### update语句未命中索引的加锁情况
类似于`select ... for update`未命中索引的情况update操作在未命中索引的情况下`也会针对主键索引的记录进行加锁,并且会获取主键索引中所有记录的锁`
其加锁情况和`select ... for update`一样,例如`update t_learn_lock set content = 'fuck' where content = 'm';`该语句并不会实际修改任何行记录但是还是会锁住主键中所有的index record。
|INDEX_NAME|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
|----------|---------|---------|-----------|---------|
||TABLE|IX|GRANTED||
|PRIMARY|RECORD|X|GRANTED|supremum pseudo-record|
|PRIMARY|RECORD|X|GRANTED|1|
|PRIMARY|RECORD|X|GRANTED|2|
|PRIMARY|RECORD|X|GRANTED|3|
|PRIMARY|RECORD|X|GRANTED|4|