锁
概述
共享锁、排他锁、行级锁、表级锁、间隙锁,这些名字并不一定单独指某一个锁,而是某个锁可能拥有的特性。
Mysql并发事务访问
读-读
读取允许并发操作
写-写
即两个事务同时操作一条数据
在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 锁 来实现的。这个所谓的锁其实是一个 内存中的结构 ,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构 和记录进行关联的。
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候就会在内存中生成一个 锁结构 与之关联。比如,事务 T1 要对这条记录做改动,就需要生成一个 锁结构与之关联:
小结几种说法:
不加锁
意思就是不需要在内存中生成对应的 锁结构 ,可以直接执行操作。
获取锁成功,或者加锁成功
意思就是在内存中生成了对应的 锁结构 ,而且锁结构的 is_waiting 属性为 false ,也就是事务可以继续执行操作。
获取锁失败,或者加锁失败,或者没有获取到锁
意思就是在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,也就是事务需要等待,不可以继续执行操作。
读-写
这种情况下可能发生 脏读 、 不可重复读 、 幻读 的问题
一般来说都是使用事务隔离解决,各个事务隔离级别的解决方式不同:
方案一:读操作利用多版本并发控制( MVCC ,下章讲解),写操作进行 加锁 。
在 读已提交、可重复读级别下,mysql使用了 MVCC读取记录
方案二:读、写操作都采用 加锁 的方式。
采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。
一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用 加锁 的方式执行。下面就讲解下MySQL中不同类别的锁。
锁的不同角度分类
概述
此处只是介绍不同类型的锁,并不是代表着mysql的每一条语句都使用了某个锁甚至全部的锁,不同的锁有不同的开启方式、有不同的应用场景。
从类型划分:读锁、写锁
- 读锁 :也称为 共享锁 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
- 写锁 :也称为 排他锁 、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
- 需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。
从粒度划分:表级锁、页级锁、行锁
表级锁
表级别的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的 S锁 或者 X锁 的。
在对某个表执行一些诸如 ALTER TABLE 、 DROP TABLE 这类的 DDL 语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。
同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行 DDL 语句也会发生阻塞。
这个过程其实是通过在 server层 使用一种称之为 元数据锁 (英文名: Metadata Locks ,简称 MDL )结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁 和 X锁 。只会在一些特殊情况下,比方说 崩溃恢复 过程中用到。比如,在系统变量 autocommit=0,innodb_table_locks = 1 时, 手动 获取InnoDB存储引擎提供的表t 的 S锁 或者 X锁 可以这么写:
LOCK TABLES t READ :InnoDB存储引擎会对表 t 加表级别的 S锁 。
LOCK TABLES t WRITE :InnoDB存储引擎会对表 t 加表级别的 X锁 。
不过尽量避免在使用InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。
InnoDB的厉害之处还是实现了更细粒度的 行锁 ,关于InnoDB表级别的 S锁 和 X锁 了解一下就可以了。
意向锁
InnoDB 支持 多粒度锁(multiple granularity locking) ,它允许 行级锁 与 表级锁 共存,而意向锁就是其中的一种 表锁 。
因为表级锁和行级锁可以并存,假设有这样一种情况:有一张表,不希望他被事务们修改,于是表上上了 S锁,此时对于表来说不能被上锁了,但是对于行来说,还是可以给行上上S锁,实现读取的需求。
那当我们想要给表上S锁时,需要保证内部每行中没有被上了X锁。如果想要给表上X锁,则行不能上锁。那么获取行的上锁信息则成了一个问题,遍历的话效率太低,所以mysql提出了意向锁的概念。
- 意向共享锁,英文名: Intention Shared Lock ,简称 IS锁 。当事务准备在某条记录上加 S锁 时,需要先在表级别加一个 IS锁 。
- 意向独占锁,英文名: Intention Exclusive Lock ,简称 IX锁 。当事务准备在某条记录上加 X锁 时,需要先在表级别加一个 IX锁 。
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。 各个锁的兼容性:
自增锁(AUTO-INC锁)
系统实现这种自动给 AUTO_INCREMENT 修饰的列递增赋值的原理主要是两个:
采用 AUTO-INC 锁,也就是在执行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值,在该语句执行结束后,再把 AUTO-INC 锁释放掉。这样一个事务在持有 AUTO-INC 锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用 INSERT … SELECT 、 REPLACE … SELECT 或者 LOAD DATA 这种插入语句,一般是使用AUTO-INC 锁为 AUTO_INCREMENT 修饰的列生成对应的值。
小贴士:
需要注意一下的是,这个AUTO-INC锁的作用范围只是单个插入语句,插入语句执行完成后,这个锁就被释放了,跟我们之前介绍的锁在事务结束时释放是不一样的。
采用一个轻量级的锁,在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。如果我们的插入语句在执行前就可以确定具体要插入多少条记录,比方说我们上边举的关于表 t 的例子中,在语句执行前就可以确定要插入2条记录,那么一般采用轻量级锁的方式对 AUTO_INCREMENT 修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。
InnoDB提供了一个称之为innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列进行赋值
当innodb_autoinc_lock_mode值为0时,一律采用AUTO-INC锁;
当innodb_autoinc_lock_mode值为2时,一律采用轻量级锁;
当innodb_autoinc_lock_mode值为1时,两种方式混着来
行级锁
记录锁
记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP 。
记录锁是有S锁和X锁之分的,称之为 S型记录锁 和 X型记录锁 。
当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
间隙锁
MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方案解决,也可以采用 加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 。InnoDB提出了一种称之为Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁 。比如,把id值为8的那条记录加一个gap锁的示意图如下。
如图中为 number 值为 8 的记录加了 gap锁 ,意味着不允许别的事务在 number 值为 8 的记录前边的 间隙插入新记录,其实就是 number 列的值 (3, 8) 这个区间的新记录是不允许立即插入的。
这个 gap锁 的提出仅仅是为了防止插入幻影记录而提出的,虽然有 共享gap锁 和 独占gap锁 这样的说法,但是它们起到的作用都是相同的。而且如果你对一条记录加了 gap锁 (不论是 共享gap锁 还是 独占gap锁 ),并不会限制其他事务对这条记录加 正经记录锁 或者继续加 gap锁 ,再强调一遍, gap锁 的作用仅仅是为了防止插入幻影记录的而已。
对于最后一行数据的后方间隙来说,innodb会在最大记录之前加入间隙锁。
关于利用间隙锁解决幻读问题:
即读取数据时对于读取的范围内数据的间隙加入间隙锁,使其他事务不能在你事务期间插入数据,即解决了幻读问题。
临键锁(Next-Key Locks)
即记录锁+间隙锁
Next-Key Locks是在存储引擎 innodb 、事务级别在 可重复读 的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。
begin;
select * from student where id <=8 and id > 3 for update;
页锁
页锁就是在 页的粒度 上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
锁的内存结构
- 锁所在的事务信息
不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个 锁结构 ,这里就记录这个事务的信息。
此锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比方说事务id等。 - 索引信息
对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。 - 表锁/行锁信息
表锁结构 和 行锁结构 在这个位置的内容是不同的:
- 表锁:
- 记载着是对哪个表加的锁,还有其他的一些信息。
- 行锁:记载了三个重要的信息:
- Space ID :记录所在表空间。
- Page Number :记录所在页号。
- n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits 属性代表使用了多少比特位。 (n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构 )
- type_mode
这是一个32位的数,被分成了 lock_mode 、 lock_type 和 rec_lock_type 三个部分
- 锁的模式( lock_mode ),占用低4位,可选的值如下:
- LOCK_IS (十进制的 0 ):表示共享意向锁,也就是 IS锁 。
- LOCK_IX (十进制的 1 ):表示独占意向锁,也就是 IX锁 。
- LOCK_S (十进制的 2 ):表示共享锁,也就是 S锁 。
- LOCK_X (十进制的 3 ):表示独占锁,也就是 X锁 。
- LOCK_AUTO_INC (十进制的 4 ):表示 AUTO-INC锁 。
在InnoDB存储引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表级锁的模式,LOCK_S和
LOCK_X既可以算是表级锁的模式,也可以是行级锁的模式。
- 锁的类型( lock_type ),占用第5~8位,不过现阶段只有第5位和第6位被使用:
- LOCK_TABLE (十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁。
- LOCK_REC (十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁。
- 行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在 lock_type 的值为
- LOCK_REC 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
- LOCK_ORDINARY (十进制的 0 ):表示 next-key锁 。
- LOCK_GAP (十进制的 512 ):也就是当第10个比特位置为1时,表示 gap锁 。
- LOCK_REC_NOT_GAP (十进制的 1024 ):也就是当第11个比特位置为1时,表示正经 记录锁 。
- LOCK_INSERT_INTENTION (十进制的 2048 ):也就是当第12个比特位置为1时,表示插入意向锁。其他的类型:还有一些不常用的类型我们就不多说了。
- is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode 这个32位的数字中:
- LOCK_WAIT (十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为false ,也就是当前事务获取锁成功。
- 其他信息 :
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。 - 一堆比特位
如果是 行锁结构 的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits 属性表示的。InnoDB数据页中的每条记录在 记录头信息 中都包含一个 heap_no 属性,伪记录 Infimum 的heap_no 值为 0 , Supremum 的 heap_no 值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no ,即一个比特位映射到页内的一条记录。