8.5 KiB
锁
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:
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检查。