MySQL 锁机制详解(表级锁、页级锁、行级锁)

MySQL 的锁知识,跟索引知识一样,都同样的复杂,甚至更复杂。所以还需要一些耐心哟!

MySQL中锁的分类

下图为MySQL部分存储引擎所支持的锁

MySQL支持三种层级的锁定

我们知道,MySQL支持三种层级的锁定,分别为:

表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁

行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大

从上到下,锁的粒度逐渐细粒化, 但实现开销逐渐增大。 同时我们也要须知,表锁,页锁,行锁并不是一个具体的锁,仅代表将数据库某个层级上的数据进行锁定。具体怎么去锁这个数据,还要看具体的锁实现是什么

如何学习MySQL中的锁知识

说实话,MySQL的锁知识,在初学的时候,真的是挺复杂的。主要是概念多,分类也多,还有各种乱七八糟,想到都烦的名词。所以学习MySQL中各种锁时,要从简单的入手,繁琐的名词暂时不要管。等有经验之后,自然会懂得。

(一) 了解MySQL大致有哪些锁,建立脑图分类

(二) 站在存储引擎的角度,具体学习存储引擎支持的锁

(三) 重点研究InnoDB行锁的具体实现算法

(四) 了解InnoDB下锁与事务,锁与索引的联系

了解了以上的内容,我想应该大致也能够应付很多日常的开发了。剩下的内容比较细致和繁琐,个人觉得还是需要实际的社会实践,才能更加的深有体会并记住。

事务持有锁的 SQL

要认识事务,SQL,与锁的知识,最好的办法就是调试。所以我这里提供给大家MySQL 8.0以及MySQL 8.0以下的查询事务持有锁情况的SQL

MySQL 8.0 以下

show engine innodb status;

在TRANSACTION位置可以看到事务持有锁的情况,但是需要经验分析,名词比较难懂

select * from information_schema.innodb_locks`; 

查看innodb引擎下,所有表所有事务的加锁情况,无法分别gap,record,next-key锁的具体类型。没有冲突情况无法显示持有的锁

select * from information_schema.innodb_locks_wait;

查看innodb引擎下,所有表所有事务因锁的阻塞情况,谁被谁block住了

MySQL 8.0

show engine innodb status;

在TRANSACTION位置可以看到事务持有锁的情况,但是需要经验分析,名词比较难懂

select * from performance_schema.data_locks`;  

查看数据库中所有表所有事务的加锁情况,没有冲突,也看到持有了什么锁,具有具体的分类,简单好用,更容易分析

select * from performance_schema.data_locks_wait;

查看数据库中,所有表所有事务因锁的阻塞情况,谁被谁block住了

所以我个人建议还是用MySQL 8.0的分析语句来调试更好,因为更加清晰,内容更多,功能更强大。

MyISAM引擎所支持的锁

MyISAM引擎的表锁

MyISAM只支持表锁,不支持行锁和页面锁。

由上图可知,表锁的具体实现有表级共享锁和表级排他锁。

MyISAM下的共享锁和排他锁

我们知道MyISAM存储引擎只支持在表层级对数据进行锁定,既只支持表锁。那么表层级具体的加锁实现有什么?既表级共享锁和表级排他锁

获得表共享读锁的事务可以读取该表的任意数据。MyISAM引擎在执行select语句前,会自动给涉及的表加表共享读锁

获得表独占写锁的事务可以更新或删除该表中任意数据。在执行update,delete,insert等语句前,会自动给涉及的表加表独占写锁

只是很多场合更喜欢把表级共享锁表级排他锁叫做表共享读锁表独占写锁,实际上它们的意思是一样的

表共享读锁和表独占写锁的兼容性

既MyISAM的表锁可以做到,读读共享,读写互斥,写写互斥的功能

值得注意的是

The LOCAL modifier enables nonconflicting INSERT statements (concurrent inserts) by other sessions to execute while the lock is held. (See Section 8.11.3, “Concurrent Inserts”.) However, READ LOCAL cannot be used if you are going to manipulate the database using processes external to the server while you hold the lock. For InnoDB tables, READ LOCAL is the same as READ

MyISAM在一定程度上还是可以支持并发的进行查询和插入操作的。既如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个线程读表的同时,另一个线程从表尾插入记录。这属于MyISAM的特性,所以InnoDB存储引擎是不支持的!

总结

虽然MySQL支持表,页,行三级锁定,但MyISAM存储引擎只支持表级的数据锁定,既表锁。所以MyISAM的加锁相对比较开销低,但数据操作的并发性能相对就不高。但如果写操作都是尾插入,那么还是可以支持一定程度的读写并发

既MyISAM的数据操作如果导致加锁行为,那么该锁肯定是一个表锁,会对全表数据进行加锁。但也要看具体什么形式的表锁,不同形式的表锁有着不同的特性

同时从MyISAM所支持的锁中也可以看出,MyISAM是一个支持读读并发,但不支持通用读写并发,写写并发的数据库引擎,所以它更适合用于读多写少的应用场合

InnoDB引擎所支持的锁

官方列举的InnoDB引擎的锁

我们从图中大概可以看出这么几种锁:

为了避免陷入名词混乱的漩涡,我们这里这说明重点,并不讲主键自增锁,空间索引断言锁之类的锁

InnoDB下的表锁和行锁

表锁和行锁

InnoDB存储引擎,不仅仅支持表级锁定,还支持行级锁定。说白了就是不仅支持表锁,还支持MyISAM所不支持的行锁。

那是不是行级锁一定比表级锁要好呢?

表锁和行锁的分类

因为我们知道,表锁和行锁并不是一把具体的锁,仅仅代表要在数据库的那个层次结构对数据进行锁定,这也代表了表,行锁还可以被细化出一下具体的锁实现

InnoDB实现的行锁

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁的实现也意味着,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

InnoDB下的共享锁和排他锁

什么是共享锁和排他锁?

事务拿到某一行记录的S锁,才可以读取这一行的数据。共享锁可以实现读读共享,读写互斥

事务拿到某一行记录的X锁,才可以修改或者删除这一行的数据。排他锁可以实现读写互斥,写写互斥

表行层次下的共享锁和排他锁

因为InnoDB支持表锁和行锁。所以在数据库层次结构的表级和行级,都可以对数据进行锁定,具体以什么方式进行锁定呢?共享锁和排他锁就是本节要讨论的具体锁实现

表级共享锁,又称为表共享读锁,既在表的层级上对数据加以共享锁,实现读读共享

表级排他锁,又称为表独占写锁,既在表的层级上对数据加以排他锁,实现读写互斥,写写互斥

行级共享锁既在行的层级上,对数据加以共享锁,实现对该行数据的读读共享

行级排他锁既在行的层级上,对数据加以排他锁,实现对该行数据的读写互斥,写写互斥

共享锁和排它锁的兼容性

一句话,就是读读共享,读写互斥,写写互斥。这里要理解一个概念,上表仅仅是同一层次的共享锁和排他锁之间的兼容性,并非是表锁和行锁共存的情况。

怎么显式地加共享锁或排他锁?

当然以上,加的是行锁的前提是,id为主键且在查询命中,否则行锁会轮为表锁

InnoDB下的意向锁

为什么会有意向锁?

InnoDB supports multiple granularity locking which permits coexistence of row locks and table locks. For example, a statement such as LOCK TABLES … WRITE takes an exclusive lock (an X lock) on the specified table. To make locking at multiple granularity levels practical, InnoDB uses intention locks.Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table

既通常情况下,表锁和行锁是相互冲突的,既获得了表锁,就无法再获得该表具体行的行锁,反之亦然。但是有的时候表锁和行锁实现部分的共存有利于更细粒度的对锁进行控制,以便得到更加的并发性能。所以InnoDB存储引擎支持多粒度(granular)锁定, 这种锁定允许一个事务中同时存在行锁和表锁。

所以InnoDB为了实现行锁和表锁共存的多粒度锁机制特性,InnoDB存储引擎也就还支持一种额外的锁方式以对多粒度锁机制进行支持,称之为意向锁(Intention Lock)

什么是意向锁呢?

并且意向锁具体实现又可以分为以下两种:

事务打算给表中的某些行加行级共享锁,事务在给某些行加S锁前必须先取得该表的IS锁

事务打算给表中的某些加行级排他锁,事务在给某些行加X锁前必须先取得该表的IX锁

总之所谓意向锁,就是提前表明了一个要加行锁的“意向”。当某表存在排他意向锁时,代表该表的某行可能存在行级排他锁。同理当某表存在共享意向锁时,代表该表的某行可能存在行级共享锁

意向锁的兼容性

意向锁因为是表级的锁,本身就是为了解决行表锁的共存,所以它是不会与行锁产生冲突的。所以上表的兼容性指代的是意向锁和表级共享锁,表级排他锁的兼容性。

怎么使用意向锁?

InnoDB下的记录锁,间隙锁,临键锁

我们的行锁根据锁的粗细粒度又可以分为三种锁,既记录锁间隙锁临键锁

什么是记录锁?

记录锁(Record Lock)就是我们最单纯认知的行锁,既只锁住一条行记录,准确的说是一条索引记录

InnoDB的行锁是依赖索引实现的,而其锁住某行数据的本质是锁住行数据对应在聚簇索引的索引记录。

此行锁非彼行锁

通常字面意思的行锁,代表对一行记录加锁。但行锁实际是一个抽象,我们平时所说 行锁(1) 还分记录锁间隙锁(Gap Lock)临键锁(Next-Key Lock)等具体的实现,但我们通常意义上单纯的 行锁(2) ,实际说的就是记录锁; 既此 行锁(1) 非彼 行锁(2)行锁(1) 更倾向是一个锁分类,代指一系列行锁 。**行锁(2)** 更接地气,更单纯,就是指锁单条记录的意思,随意不同场景,不同语义下,所代表的意思可能有些许不同;但通常情况下,都会被混在一起,所以也不用太介意,自己要清楚就好

什么是间隙锁?

间隙锁(Gap Lock),顾名思义,它会封锁索引记录中的“缝隙”,不让其他事务在“缝隙”中插入数据。它锁定的是一个不包含索引本身开区间范围 (index1,index2)。间隙锁是封锁索引记录之间的间隙,或者封锁第一条索引记录之前的范围,又或者最后一条索引记录之后的范围

**说白了,间隙锁的目的就是为了防止索引间隔被其他事务的 “插入”**。间隙锁是InnoDB权衡性能并发性后出来的特性。

间隙锁之间是互相兼容的,既不同事务的Gap锁是可以共存的,可以存在重合区域或是完全重合。多个事务的Gap锁重合代表多个事务都想阻止其他事务往该索引间隙 "插入" 数据。也因为Gap一次就对插入行为锁住了一片的区域,不利于多事务并发写。所以后续MySQL也就提供了一种特殊的间隙锁(插入意向锁),以解决并发写的问题

间隙锁实际也分共享间隙锁,排他间隙锁,虽然有分类,但事实上他们的意义是等价毫无区别的,所以可以忽略这一层,只要知道它们的作用都是阻止其他事务往间隙插入数据。

间隙锁只发生在事务隔离级别为RR(Repeatable Read)的情况下,它用于在隔离级别为RR时,阻止幻读(phantom row)的发生;隔离级别为RC时,搜索和索引扫描时,Gap锁是被禁用的,只在外键约束检查和 重复key检查时Gap锁才有效,正是因为此,RC时会幻读问题。

比如某个事务执行select * from table where id between 10 and 20 for update;语句,当其他事务往表里 “插入” id在(10,20)之间的值时,就会被(10,20)的间隙锁给阻塞

什么是临键锁?

临键锁(Next-Key Lock)其实并不能算是一把新的行锁,其实际就是记录锁(Record Lock)和间隙锁(Gap Lock)的组合,既封锁了"缝隙",又封锁了索引本身,既组合起来构成了一个半开闭区间的锁范围。既临键锁锁的是索引记录本身,以及索引记录之前的间隙(index1, index2 )

比如某表有4行数据,主键分别为10,11,13,20。那么该表可能存在的临键锁锁住聚簇索引的区间如下

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

每两个索引项之间就是一个next key锁区域,通常规则是 左开右闭除了最后一个锁区间,是全开区间,代表锁住索引最大项之后的区域

记录锁,间隙锁,临键锁的关系

我们知道InnoDB的行锁默认是基于B+Tree的。所以行锁依赖的索引是有序的

排他,共享锁与记录锁,间隙锁,临键锁的关系

的确,我们一直都说行锁是一个抽象,只代表在行层级对数据进行锁定。我们也说行锁实际有具体的实现,比如排他锁和共享锁。为什么这个时候又冒出了一些记录锁,间隙锁,临键锁的概念?!这是在懵我读书少吗?

其实排他锁,共享锁与记录锁,间隙锁,临键锁本不应该混淆在一起的,因为他们虽然都属于行锁的范畴,但却是不同维度(角度)的概念。

所以它们只是不同角度分类下的概念,比如我们可以这么认为,如果一把行锁在锁粒度上的实现是一个记录锁,那么该行锁就是一把记录锁,而这个记录锁在功能上的实现是一把共享锁,那么这把记录锁就是一把共享锁。**在这个角度上分析,此时这把行锁即是记录锁,也是共享锁。

InnoDB下的插入意向锁

什么是插入意向锁?

之前,我们学习了什么是意向锁,IS,IX的其中一个好处是兼容行级S锁和表级S锁,做到真正的读读共享。那什么是插入意向锁呢?插入意向锁一种特殊的间隙锁,属于行锁。其实插入意向锁的好处也类似,但要做到的功能并非读读共享,而是针对某个区间范围做到写写共享,但这个写写并非对同一行记录而言,而是针对某个区间范围内的写入可以并发执行,只要写入的并非同一索引项(行)。

说白了,就是插入意向锁允许多个事务的插入写操作在某个索引区间无需等待的并发执行,只要操作的不是同一行的数据

插入意向锁和意向锁的关系

插入意向锁和Gap锁的关系

相同点

不同点

插入意向锁兼容性

以下我们来说说插入意向锁的兼容性

插入意向锁的作用?以及为何替代Gap锁

我们已经知道了插入间隙锁最主要的作用就是站在多事务操作的不是同一行数据的基础上,可以让某个索引区间范围做到并发的插入写操作。那么为什么又拿插入意向锁这种特殊的Gap锁去代替普通Gap锁呢?这是有什么独特之处吗?

1.在了解插入意向锁作用之前,你得明白事务的插入行为会造成什么?

我们肯定简单的知道,当某个事务插入一个新行时,肯定要获取对该行的排他记录锁。看上去好像没有毛病。问题是,在新行没有生产前,怎么获取对该新行的排他记录锁? 明显就不能嘛,所以一个插入行为,在获取对新行的排他记录锁之前,我们需要先获取新行所在的索引区间的锁,既间隙锁。

既一个插入单行数据的行为,必须先获得一个粗粒度的锁,才能安全的保证新行生成前,没有其他事务插入同样的新行(避免冲突)。

间隙锁在这里的作用就是阻塞企图在该索引间隙中插入数据的其他事务

2.所以我们明白了,一个插入行为,必须要先获得粗粒度的间隙锁,才能保证生成新行,再获取细粒度该行的排他记录锁。

综上,在插入行为方面,Gap锁在这方面的功能最终是被插入意向锁所代替的,插入意向锁就是为了改善Gap锁在数据插入方面的同步开销而引入的特殊Gap锁

乐观锁和悲观锁

所以要是别人再问你乐观锁和悲观锁是什么,你千万别说它是一种具体的锁,它只是一种锁的设计思想,他可以有很多具体的实现类

悲观锁

什么是悲观锁?

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法; 悲观锁指的是采用一种持悲观消极的态度,默认数据被外界访问时,必然会产生冲突,所以在数据处理的整个过程中都采用加锁的状态,保证同一时间,只有一个线程可以访问到数据,实现数据的排他性;通常,数据库的悲观锁是利用数据库本身提供的锁机制去实现的.

数据库的悲观并发控制可以解决读-写冲突和写-写冲突,指在用加锁的方式去解决

悲观锁的实现

通常情况下,数据库的悲观锁就是利用数据库本身提供的锁去实现的

当然数据库提供了非常多的锁,每种数据库提供的锁也不尽然相同,所以具体情况就要看是什么锁了,比如行锁,表锁等

优点与缺点

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

优点: 适合在写多读少的并发环境中使用,虽然无法维持非常高的性能,但是在乐观锁无法提更好的性能前提下,可以做到数据的安全性

缺点: 加锁会增加系统开销,虽然能保证数据的安全,但数据处理吞吐量低,不适合在读书写少的场合下使用

乐观锁

什么是乐观锁?

在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法;乐观锁( Optimistic Locking ) 是相对悲观锁而言,乐观锁是假设认为即使在并发环境中,外界对数据的操作一般是不会造成冲突,所以并不会去加锁(所以乐观锁不是一把锁),而是在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回冲突信息,让用户决定如何去做下一步,比如说重试,直至成功为止;数据库的乐观锁,并不是利用数据库本身的锁去实现的,可能是利用某种实现逻辑去实现做到乐观锁的思想

数据库的乐观并发控制要解决的是数据库并发场景下的写-写冲突,指在用无锁的方式去解决

CAS思想

其实数据库乐观锁的具体实现几乎就跟Java中乐观锁采用的CAS算法思想是一致,所以我们可以从CAS算法中学习到数据库乐观锁的设计:

CAS指令全称为Compare and Swap,它是系统的指令集,整个CAS操作是一个原子操作,是不可分割的。从具体的描述上,我们可以这么看CAS操作:

CAS指令需要3个操作数,分别是内存位置V,旧的预期值A,和新值B。CAS指令执行时,当我们读取的内置位置V的现值等于旧预期值A时,处理器才会将新值B去更新内置位置V的值。否则它就不执行更新,但无论是否更新V的值,都会返回V的旧值。

我们通俗的放到代码层次上去理解i = 2; i++,就是说:

数据库层的乐观锁实现也类似代码层面的实现。

数据库中乐观锁的实现

通常乐观锁的实现有两种,但它们的内在都是CAS思想的设计:

方式一: 使用数据版本(version)实现

update table set num = num + 1 , version = version + 1 where version = #{version} and id = #{id}

方式二: 使用时间戳(timestamp)实现

update table set num = num + 1 ,update_time = unix_timestamp(now()) where id = #{id} and update_time = #{updateTime}

但是我们要注意的是,要实现乐观锁的思想的同时,我们必须要要保证CAS多个操作的原子性,即获取数据库数据的版本,拿数据库的数据版本与之前拿到的版本的比较,以及更新数据等这几个操作的执行必须是连贯执行,具有复合操作的原子性;所以如果是数据库的SQL,那么我们就要保证多个SQL操作处于同一个事务中

优点与缺点

优点:

在读多写少的并发场景下,可以避免数据库加锁的开销,提高Dao层的响应性能
其实很多情况下,我们orm工具都有带有乐观锁的实现,所以这些方法不一定需要我们人为的去实现

缺点:

在写多读少的并发场景下,即在写操作竞争激烈的情况下,会导致CAS多次重试,冲突频率过高,导致开销比悲观锁更高

乐观锁和悲观锁的抉择

对乐观锁和悲观锁的抉择主要体现在写-写

在悲观锁和乐观锁的抉择中,我们可以从下面三个因素来考虑:

所以我们知道:

OCC,PCC,MVCC三者的关系

来源:https://blog.csdn.net/SnailMann/article/details/88353099

展开阅读全文

页面更新:2024-03-13

标签:间隙   悲观   详解   意向   索引   乐观   冲突   机制   事务   操作   数据库   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top