Files
rikako-note/mysql/mysql文档/锁.md
2024-11-13 12:30:41 +08:00

8.5 KiB
Raw Blame History

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 updatelock in share mode在读取时添加X锁或S锁时读取操作才会被阻塞。

并且,for updatelock 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检查。