很多小伙伴包括我自己在面试当中经常会被问到MySQL关于锁的知识。本文就详细带大家来介绍一下MySQL中的锁相关知识。
首先来准备一点基础知识吧。一些老生常谈的问题
- MySQL的隔离界别有哪些
- 读未提交(Read Uncommitted):一个事务可以看到另一个正在进行中的事务尚未提交的修改。
- 读已提交(Read Committed):一个事务只能看见已经提交的事务所做的修改。
- 可重复读(Repeatable Read):在同一个事务中,多次读取同一数据会返回相同的结果。即使其他事务修改了这个数据,该事务也会在提交前一直使用它自己所读取的数据。
- 串行化(Serializable):最高的隔离级别,所有事务按照顺序依次执行。每个事务都必须等待其他事务完成后才能开始执行。这样可以避免脏读、不可重复读和幻影读的问题,但是会影响性能。
- MySQL并发执行可能会遇到以下问题:
- 脏读(Dirty Read):一个事务读取到另一个尚未提交的事务的数据,如果这个事务回滚了,则读取到的数据就是无效的。
- 不可重复读(Non-Repeatable Read):在同一个事务中,多次读取同一份数据时,由于其他事务进行了修改,每次读取得到的结果都不同。这种情况通常发生在隔离级别为“读已提交”的情况下。
- 幻读(Phantom Read):在同一个事务中,多次执行相同的查询语句时,由于其他事务进行了插入或删除操作,每次返回的结果集都不同。这种情况通常发生在隔离级别为“可重复读”和“串行化”的情况下。
- 死锁(Deadlock):当两个或多个事务同时持有某些资源,并且都在等待其他事务释放它们所需要的资源时,就会发生死锁。如果没有外部干预,这些事务将永远阻塞下去。
| 脏写 | 脏读 | 不可重复读 | 幻读 | |
|---|---|---|---|---|
| 读未提交 | √ | × | × | × |
| 读已提交 | √ | √ | × | × |
| 可重复读 | √ | √ | √ | × |
| 可串行化 | √ | √ | √ | √ |
以上内容都是八股文的四问题了 。这里就不做过多解释了。但是这里介绍一下不同的隔离界别是如下解决脏读、不可重复读、幻读的问题的。
方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁
所谓的 MVCC 就是通过生成一个 ReadView ,然后通过 ReadView 找到符合条件的记录版本(历史版本是由 undo日志 构建的),查询语句只能读到在生成 ReadView 之前已提交事务所做的更改,在生成 ReadView 之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯 定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用 MVCC 时, 读-写 操作并不冲突。
方案二:读、写操作都采用 加锁 的方式。
MySQL中锁结构
其实MySQL里面锁结构有特别特比特多。。有兴趣的同学可以具体研究一下。这里就介绍一下我认为最重要的俩个trx信息和is_waiting
- trx信息:代表这个锁结构由哪个事务生成
- is_waiting:代表当前事务是否在等待。如果false的话代表当前没有事务获取锁。可以直接获取。如果是true的话。代表当前有其它事物获取了锁。需要等待。
一致性读(Consistent Reads)
事务利用MVCC进行的读取操作称之为一致性读(或者称之为快照读),所有普通的select语句在READ COMMITED、REPEATABLE READ隔离级别下都算是一致性读。(这句话是重点啊。)一致性读并不会对表中的人物记录加锁操作,其他事务可以自由的对表中的记录做改动
这就是我常用的普通的select语句
SELECT * FROM t;
SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
锁定读(Locking Reades)
共享锁和独占锁
- 共享锁,(Shared Locks)简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。
- 独占锁,也常称拍他锁,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁
假如事务T1首先获取了一条记录的S锁,这时候事务T2也要访问这条记录
- 如果事务T2想在获取一个记录的S锁。那么T2也会获得该锁。这时T1和T2在该记录同时获取S锁
- 如果T2想要获取该记录的X锁。就会被阻锁。直到事务T1提交之后将S锁释放掉
所以S锁和S锁兼容,S锁和X锁不兼容,X锁和X锁也不兼容
| 兼容性 | X | S |
|---|---|---|
| X | 不兼容 | 不兼容 |
| S | 不兼容 | 兼容 |
锁定读的语句
- 对读取的记录加S锁
SELECT ... LOCK IN SHARE MODE
- 对读取记录加X锁
SELECT ... FOR UPDATE;
写操作
平常写操作就是DELETE、UPDATE、INSERT这三种
- DELETE: 其实对一条记录做DELETE操作的的过程就是先在B+树中定位到这条记录的位置。获取这条记录的X锁,然后对这条记录执行delete mark。所以DELETE其实就是在B+树定位记录的位置是获取了一个X锁的锁定读
- UPDATE:
对一条记录做UPDATE错做时分为三种情况:
- 如果未修改该记录的键值并且被更新的列占用的存储空间没有发生变化。则就是先获取记录在B+树中的位置。然后获取一下X锁。最后再修改。所以我们可以把定位带修改记录的过程看成是一种获取X锁的锁定读
- 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+ 树中定位到这条记录的位置,然后获取一下记录的 X锁 ,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X 锁 的 锁定读 ,新插入的记录由 INSERT 操作提供的 隐式锁 进行保护。
- 如果修改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作,加锁操作就需要按照 DELETE 和 INSERT 的规则进行了。
- INSERT: 一般情况下,新插入一条记录的操作并不加锁, InnoDB 的大叔通过一种 隐式锁 来保护这条新插入的记录在本事务提交前不被别的事务访问,
多粒度锁
上面提到的锁都是针对记录(其实就是行)的,也可以称之为行锁。众所周知一个事务也可以对一个表加锁,称之为表锁。表锁也可以分为共享锁(S)和独占锁(X)
其余的就和我们上面介绍的一致了S锁和S锁兼容,S锁和X锁不兼容,X锁和X锁也不兼容。多了就不废话了。但是!!但是啊这里面有个问题。如果我们对一个表上X锁。如果这个表中有的记录已经被上了S锁呢。或者是上了X锁的。或者我们对表上S锁。如果表中有的记录已经上了X锁呢。这种怎么办。不能上锁之前一行一行去遍历吧。告诉大家。不遍历。这辈子都不可能遍历的。因此InnoDB又提出了一种锁的概念。意向锁
- 意向共享锁(Intentio Shared Lock),简称IS锁。当事务准备对表中的某条记录上S锁的时候。会先在表上加一个IS锁
- 意向独占锁:简称IX锁。当事务准备在某条记录上加 X锁时,需要先在表级别加一个 IX锁 。
现在大家应该懂了吧。当如果需要给表上锁的话。就先判断一下当前表的意向锁是那种。那不就OK了嘛 。完美解决。
总结一下:IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。
| 兼容性 | X | IX | S | IS |
|---|---|---|---|---|
| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
| IX | 不兼容 | 兼容 | 不兼容 | 兼容 |
| S | 不兼容 | 不兼容 | 兼容 | 兼容 |
| IS | 不兼容 | 兼容 | 兼容 | 兼容 |
InnoDB存储引擎中的锁
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锁
表级别的 IS锁 、 IX锁
这块上面已经给大家介绍过了。就不墨迹了。
表级别的AUTO-INC锁
这个有的小朋友可能是第一次见吧。但是确实我们最经常用的了 。。。每次创建表的时候都会为某个列添加AUTO_INCREMENT属性。??一下子想起来了吧。这不就是主键递增嘛。让我们看一下主键递增是如何实现的其实主要原理有俩个
- 采用 AUTO-INC 锁,也就是在执行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值,在该语句执行结束后,再把 AUTO-INC 锁释放掉。这样一个事务在持有 AUTO-INC 锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。这种情况一般是我们执行前不确定要插入多少条记录。会采用AUTO-INC锁。
- 采用一个轻量级锁。在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。这种情况一般都是我们提前知道执行前要插入多少条。就会采用轻量级锁。
InnoDB中的行级锁(重中之重!!!!!!)
Record Locks
Recod Locks全程LOCK_REC_NOT_GAP是一种行级锁定类型,用于锁定记录而不锁定间隙。具体来说,它在锁定记录时会防止其他事务插入新记录或者更新已存在的记录,但是不会阻止其他事务在记录之间插入新记录。Recod Locks也分为S型锁和X型锁。
Gap Locks
官方名称为:Lock_GAP。LOCK_GAP 是一种间隙锁(gap lock)。间隙锁是用于控制事务并发时,防止出现幻读(Phantom Read)的情况。如果一个事务执行了一条 SELECT 语句并获得了间隙锁,那么其他事务就无法在该间隙中插入新的数据,直到这个事务释放了锁。间隙锁只在默认的事务隔离级别 REPEATABLE READ 和 SERIALIZABLE 下起作用,且仅适用于非唯一索引。而在 READ COMMITTED 隔离级别下,MySQL 不会使用间隙锁,所以可能会出现幻读问题。
Next-Key Locks :
Next-Key官方称之为LOCK_ORDINARY。next-key锁的本质就是一个正经记录锁和一个 gap锁 的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙 。其实也就是我们常说的临键锁。
Insert Intention Locks
官方称之为LOCK_INSERT_INTENTION 也可以称为插入意向锁。有时候一个事务在插入一条记录时需要判断一下插入位置是不是被别的事物加了所谓的gap锁。如果有的话需要等待。如果需要等待InnoDB在这种情况下也需要在内存中生成一个锁结构。表示想插入但是现在在等待。
隐式锁。
说一个事务在执行 INSERT 操作时,如果即将插入的 间隙 已经被其他事务加了 gap锁 ,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个 插入意向锁 ,否则一般情况下 INSERT 操作是不加锁的。但是为了保证插入时候的安全性。InnoDB的通过隐藏列trx_id也就是事务id来实现了隐式锁。也就是向聚簇索引插入一条记录的时候会先判断该记录的事务id是否是当前活跃的事务,如果是的话,就会帮助当前事务创建一个X锁。然后自己进入等待状态。对于二级索引来说没有trx_id隐藏列,,但是在页面PageHeader部分有个PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重聚簇索引的做法。
InnoDB锁的内存结构
其实对一条记录加锁的本质就是内存中创建一个锁结构与之关联。但是肯定不是说如果一个事务总对多条记录加锁的话就会创建一个多个锁结构的。那如果你要是获取成千上万条锁记录然后创建那么多锁结构那不完了嘛。废废了嘛。因此InnoDB中如果符合以下这些条件那么这些记录的锁就会被放到一个锁结构中。
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面中
- 加锁的类型是一样的
- 等待状态是一样的
让我们先看一下InnoDB存储引擎事务锁结构吧。方便大家理解
- 锁所在的事务信息:在事务执行过程中,哪个事务生成了这个锁结构
-
- 索引信息:对于行锁来说,需要记录一下加锁的记录属于哪个索引的
- 表锁/行锁信息
- 表锁:记载着这是对哪个表加的锁,还有其他的一些信息。
- 行锁:记载了三个重要的信息:
- Space ID :记录所在表空间。
- Page Number :记录所在页号。
- n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个 n_bits 属性代表使用了多少比特位。
- type_node 这是一个32位的数,被分成了lock_mode、lock_type和rec_lock_type。这块就不太详细讲了。大家知道个大概就可以。lock_mode就是存的锁模式,就比如是IS、IX、或者是S锁,X锁,AUTO-INC锁等等。lock_type就是锁的类型,是行级锁还是表级锁。rec_lock_type:行锁的具体类型,是next-key锁还是gap锁等等等。
这块举一个简单的例子吧。假如现在我们有个test表。表中有个列是name。假如我们现在有个事务T1想对name=‘互联网小趴菜’这条记录加一个S型行锁,这条记录设这些记录存储在所在的表空间号为 32 ,页号为 2 的页面上。
- 首先肯定是先加标级别的IS锁。这里也会生成一个表级别锁的内存结构。不过这里我们就不关心表级锁了。主要分析一下行级锁的过程
- T1要加锁,所以锁结构的锁所在事务信息指的就是T1.
- 直接对聚餐索引加锁。所以索引信息指的就是PRIMARY索引列
- 由于是行锁。所以需要记录三个重要信息Space ID :表空间32,Page Number :2。n_bits: n_bits有个公式n_bits = (1 + ((n_recs + LOCK_PAGE_BITMAP_MARGIN) / 8)) * 8。其中 n_recs 指的是当前页面中一共有多少条记录(算上伪记录和在垃圾链表中的记录),比方说现在 hero 表一共有7条记录(5条用户记录和2条伪记录),所以 n_recs 的值就是 7 , LOCK_PAGE_BITMAP_MARGIN 是一个固定的值 64 ,所以本次加锁的 n_bits 值就是:n_bits = (1 + ((7 + 64) / 8)) * 8 = 72
- type_mode 是由三部分组成的:lock_mode ,这是对记录加 S锁 ,它的值为 LOCK_S 。lock_type ,这是对记录进行加锁,也就是行锁,所以它的值为 LOCK_REC 。rec_lock_type ,这是对记录加 Record Locks S锁 ,也就是类型为 LOCK_REC_NOT_GAP 的锁。另外,由于当前没有其他事务对该记录加锁,所以应当获取到锁,也就是 LOCK_WAIT 代表的二进制位应该是0。type_mode = LOCK_S | LOCK_REC | LOCK_REC_NOT_GAP也就是:type_mode = 2 | 32 | 1024 = 1058。
这块大家暂时了解一下就好。如果真的要深究的话。。。。。。那还得再开一个专栏来整。
本文主要介绍了MySQL在InnoDB存储引擎下有哪些锁。锁的概念一些知识。可能有的小朋友看了以后觉得嗯~~~我懂了。但是在具体SQL的时候还是不知道我的这条SQL到底加了什么锁。后续会专门出一篇文章就是针对不同SQL加了到底哪些锁的。感兴趣的话可以关注我哦!!!!!!以上就是我个人对锁的一些总结。有不对的地方麻烦各位大神指出哦。小弟不胜感谢!!!!