导言

本文为社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

锁!这个词汇在编程中呈现的次数尤为频繁,简直主流的编程言语都会具有完善的锁机制,在数据库中也并不例外,为什么呢?这儿牵扯到一个关键词:高并发,因为现在的计算机领域简直都是多核机器,因而再编写单线程的应用天然无法将机器功用发挥到最大,想要让程序的并发性越高,多线程技能天然就呼之欲出,多线程技能一方面能充分压榨CPU资源,另一方面也能提高程序的并发支撑性。

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

多线程技能尽管能带来一系列的优势,但也因而引发了一个丧命问题:线程安全问题,为了处理多线程并发履行形成的这个问题,然后又引出了锁机制,经过加锁履行的方法处理这类问题。

多线程、线程安全问题、锁机制,这都是我们的老朋友了,信任之前曾仔细读过《并发编程系列》相关文章的小伙伴都并不生疏,而本章则首要解说MySQL中供给的锁机制。

一、MySQL锁的由来与分类

客户端发往MySQL的一条条SQL句子,实际上都可以了解成一个个独自的业务,而在前面的《MySQL业务篇》中提到过:业务是根据数据库衔接的,而每个数据库衔接在MySQL中,又会用一条作业线程来维护,也意味着一个业务的履行,本质上便是一条作业线程在履行,当呈现多个业务一起履行时,这种状况则被称之为并发业务,所谓的并发业务也便是指多条线程并发履行。

多线程并发履行天然就会出问题,也便是《业务篇-并发业务问题》中聊到的脏写、脏读、不可重复读以及幻读问题,而关于这些问题又可以经过调整业务的阻隔等级来避免,那为什么调整业务的阻隔等级后能避免这些问题产生呢?这是因为不同的阻隔等级中,作业线程履行SQL句子时,用的锁粒度、类型不同。

也便是说,数据库的锁机制自身是为了处理并发业务带来的问题而诞生的,首要是保证数据库中,多条作业线程并行履行时的数据安全性。

但要先弄理解一点,所谓的并发业务肯定是根据同一个数据而言的,例如业务A现在在操作X表,业务B在操作Y表,这是一个并发业务吗?答案明显并不是,因为两者操作的都不是同一个数据,没有同享资源天然也不会形成并发问题。多个业务一起操作一张表、多个业务一起操作同一行数据等这类情景,这才是所谓的并发业务。

1.1、MySQL锁机制的分类

MySQL的锁机制与索引机制相似,都是由存储引擎负责完结的,这也就意味着不同的存储引擎,支撑的锁也并不同,这儿是指不同的引擎完结的锁粒度不同。但除开从锁粒度来区分锁之外,其实锁也可以从其他的维度来区分,因而也会造出许多关于锁的名词,下面先简略整理一下MySQL的锁系统:

  • 以锁粒度的维度区分:
    • ①表锁:
      • 大局锁:加上大局锁之后,整个数据库只能答应读,不答应做任何写操作。
      • 元数据锁 / MDL锁:根据表的元数据加锁,加锁后整张表不答应其他业务操作。
      • 意向锁:这个是InnoDB中为了支撑多粒度的锁,为了兼容行锁、表锁而规划的。
      • 自增锁 / AUTO-INC锁:这个是为了提高自增ID的并发刺进功用而规划的。
    • ②页面锁
    • ③行锁:
      • 记载锁 / Record锁:也便是行锁,一条记载和一行数据是同一个意思。
      • 空地锁 / Gap锁:InnoDB中处理幻读问题的一种锁机制。
      • 临建锁 / Next-Key锁:空地锁的升级版,一起具有记载锁+空地锁的功用。
  • 以互斥性的维度区分:
    • 同享锁 / S锁:不同业务之间不会相互排挤、可以一起获取的锁。
    • 排他锁 / X锁:不同业务之间会相互排挤、一起只能答应一个业务获取的锁。
    • 同享排他锁 / SX锁:MySQL5.7版别中新引进的锁,首要是处理SMO带来的问题。
  • 以操作类型的维度区分:
    • 读锁:查询数据时运用的锁。
    • 写锁:履行刺进、删去、修正、DDL句子时运用的锁。
  • 以加锁方法的维度区分:
    • 显现锁:编写SQL句子时,手动指定加锁的粒度。
    • 隐式锁:履行SQL句子时,根据阻隔等级主动为SQL操作加锁。
  • 以思维的维度区分:
    • 达观锁:每次履行前以为自己会成功,因而先测验履行,失利时再获取锁。
    • 失望锁:每次履行前都以为自己无法成功,因而会先获取锁,然后再履行。

放眼望下来,是不是看着还蛮多的,但总归说来说去其实就同享锁、排他锁两种,只是加的方法不同,加的当地不同,因而就演化出了这么多锁的称号。

二、同享锁与排他锁

同享锁又被称之为S锁,它是Shared Lock的简称,这点很简略了解,而排他锁又被称之为X锁,关于这点我则不太了解,因为排他锁的英文是Exclusive Lock,居然不叫E锁,反而叫X锁,究竟是红杏出了墙仍是…..,打住,回归话题自身来聊一聊这两种锁。

其实有些当地也将同享锁称之为读锁,排他锁称之为写锁,这乍一听并没啥问题,究竟对同一数据做读操作是可以同享的,写则是不答应。但这个说法并不完全正确,因为读操作也可以是排他锁,即读操作产生时也不答应其他线程操作,而MySQL中也的的确确有这类场景,比方:
一条线程在读数据时加了一把锁(读锁),此刻当别的一条线程来测验对相同数据做写操作时,这条线程会堕入堵塞,因为MySQL中一条线程在读时不答应其他线程改。
在上述这个事例中,读锁显着也会存在排挤写操作,因而前面说法并不正确,同享锁便是同享锁,排他锁便是排他锁,不能与读锁、写锁混为一谈

2.1、同享锁

同享锁的意思很简略,也便是不同业务之间不会排挤,可以一起获取锁并履行,这就相似于之前聊过的《AQS-同享方法》,但这儿所谓的不会排挤,仅仅只是指不会排挤其他业务来读数据,但其他业务测验写数据时,就会呈现排挤性,举个比方了解:

业务T1ID=88的数据加了一个同享锁,此刻业务T2、T3也来读取ID=88的这条数据,这时T2、T3是可以获取同享锁履行的,但此刻又来了一个业务T4,它则是想对ID=88的这条数据履行修正操作,此刻同享锁会呈现排挤行为,不答应T4获取锁履行。

MySQL中,我们可以在SQL句子后加上相关的关键字来运用同享锁,语法如下:

SELECT ... LOCK IN SHARE MODE;
-- MySQL8.0之后也优化了写法,如下:
SELECT ... FOR SHARE;

这种经过在SQL后增加关键字的加锁方法,被称为显式锁,而实际上为数据库设置了不同的业务阻隔等级后,MySQL也会对SQL主动加锁,这种方法则被称之为隐式锁。

此刻来做个关于同享锁的小试验,先打开两个cmd窗口并与MySQL建立衔接:

-- 窗口1:
-- 敞开一个业务
begin;
-- 获取同享锁并查询 ID=1 的数据
select * from `zz_users` where user_id = 1 lock in share mode;
-- 窗口2:
-- 敞开一个业务
begin;
-- 获取同享锁并查询 ID=1 的数据
select * from `zz_users` where user_id = 1 lock in share mode;

此刻两个业务都是履行查询的操作,因而可以正常履行,如下:

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

紧接着再在窗口2中,测验修正ID=1的数据:

-- 修正 ID=1 的姓名为 猫熊
update `zz_users` set `user_name` = "猫熊" where `user_id` = 1;

此刻履行后会发现,窗口2没了反应,这条写SQL明显并未履行成功,如下:

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

明显当另一个业务测验对具有同享锁的数据进行写操作时,会被同享锁排挤,因而从这个试验中可以得知:同享锁也具有排他性,会排挤其他测验写的线程,当有线程测验修正同一数据时会堕入堵塞,直至持有同享锁的业务完毕才干持续履行,如下:
(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

当第一个持有同享锁的业务提交后,此刻第二个业务的写操作才干持续往下履行,从上述截图中可显着得知:第二个业务/线程被堵塞24.74s后才履行成功,这是因为第一个业务迟迟未完毕导致的。

2.2、排他锁

上面简略的了解了同享锁之后,紧着来看看排他锁,排他锁也被称之为独占锁,也便是相似于之前所讲到的《AQS-独占方法》,当一个线程获取到独占锁后,会排挤其他线程,如若其他线程也想对同享资源/同一数据进行操作,有必要比及当时线程开释锁并竞争到锁资源才行。

值得留意的一点是:排他锁并不是只能用于写操作,关于一个读操作,我们也可以手动的指定为获取排他锁,当一个业务在读数据时,获取了排他锁,那当其他业务来读、写同一数据时,都会被排挤,比方业务T1ID=88的这条数据加了一个排他锁,此刻T2来加排他锁读取这条数据,T3来修正这条数据,都会被T1排挤。

MySQL中,可以经过如下方法显式获取独占锁:

SELECT ... FOR UPTATE;

也简略的做个小试验,如下:

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

当两个业务一起获取排他锁,测验读取一条相同的数据时,其间一个业务就会堕入堵塞,直至另一个业务完毕才干持续往下履行,可是下述这种状况则不会被堵塞:
(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

也便是另一个业务不获取排他锁读数据,而是以一般的方法读数据,这种方法则可以立刻履行,Why?是因为读操作默许加同享锁吗?也并不是,因为你测验加同享锁读这条数据时依旧会被排挤,如下:
(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

可以显着看到,第二个业务中测验经过加同享锁的方法读取这条数据,依旧会堕入堵塞状况,那前面究竟是因为啥原因才导致的能读到数据呢?其实这跟另一种并发操控技能有关,即MVCC机制(下篇再深入剖析)。

2.3、MySQL锁的开释

等等,似乎在我们前面的试验中,每次都仅获取了锁,但如同从未开释过锁呀?其实MySQL中开释锁的动作都是隐式的,究竟假如交给我们来开释,很简略因为操作不妥形成死锁问题产生。因而关于锁的开释作业,MySQL自己来干,就相似于JVM中的GC机制相同,把内存开释的作业留给了自己完结。

但关于锁的开释时机,在不同的阻隔等级中也并不相同,比方在“读未提交”等级中,是SQL履行完结后就立马开释锁,而在“可重复读”等级中,是在业务完毕后才会开释。

OK~,接下来一起来聊一聊MySQL中不同粒度的锁,即表锁、行锁、页锁等。

三、MySQL表锁

表锁应该是听的最多的一种锁,因为完结起来比较简略,一起应用规模也比较广泛,简直全部的存储引擎都会支撑这个粒度的锁,比方常用的MyISAM、InnoDB、Memory等各大引擎都完结了表锁。

但要留意,不同引擎的表锁也在完结上以及加锁方法上有少许不同,但归根结底,表锁的意思也就以表作为锁的基础,将锁加在表上,一张表只能存在一个同一类型的表锁。

上面这段话中提到过,不同的存储引擎的表锁在运用方法上也有些不同,比方InnoDB是一个支撑多粒度锁的存储引擎,它的锁机制是根据聚簇索引完结的,当SQL履行时,假如能在聚簇索引射中数据,则加的是行锁,如无法射中聚簇索引的数据则加的是表锁,比方:

select * from `zz_users` for update;

这条SQL就无法射中聚簇索引,此刻天然加的便是表等级的排他锁,可是这个表级锁,并不是真实意义上的表锁,是一个“伪表锁”,但作用是相同的,锁了整张表。

而反观MyISAM引擎,因为它并不支撑聚簇索引,所以无法再以InnoDB的这种方法去对表上锁,因而如若要在MyISAM引擎中运用表锁,又需求运用额定的语法,如下:

-- MyISAM引擎中获取读锁(具有读-读可同享特性)
LOCK TABLES `table_name` READ;
-- MyISAM引擎中获取写锁(具有写-读、写-写排他特性)
LOCK TABLES `table_name` WRITE;
-- 检查现在库中创立过的表锁(in_use>0表明现在正在运用的表锁)
SHOW OPEN TABLES WHERE in_use > 0;
-- 开释已获取到的锁
UNLOCK TABLES;

如上便是MyISAM引擎中,获取表等级的同享锁和排他锁的方法,但这儿的关键词其实叫做READ、WEITE,翻译过来也便是读、写的意思,因而关于同享锁便是读锁、排他锁便是写锁的说法,估量便是因而而来的。

不过MyISAM引擎中,获取了锁还需求自己手动开释锁,不然会形成死锁现象呈现,因为假如不手动开释锁,就算业务完毕也不会主动开释,除非当时的数据库衔接中断时才会开释。

此刻来观察一个小试验,代码和过程就不贴了,要点看图,如下:

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

如若你自己有兴趣,也可以按照上图中的序号一步步试验,从这个试验成果中,明显能佐证我们前面抛出的观点,MyISAM表锁显式获取后,有必要要自己主动开释,不然结合数据库衔接池,因为数据库衔接是长存的,就会导致表锁一直被占用。

这儿还漏了一个小试验,也便是当你加了read读锁后,再测验加write写锁,就会发现无法获取锁,当时线程会堕入堵塞,反过来也是同理,但我就不再从头再弄了,究竟这个图再搞一次就有点累~

OK~,到这儿就对InnoDB、MyISAM引擎中的表锁做了简略介绍,但实际上除开最基本的表锁外,还有其他几种表锁,即元数据锁、意向锁、自增锁、大局锁,接下来一起来聊一聊这些特别的锁。

3.1、元数据锁(Meta Data Lock

Meta Data Lock元数据锁,也被简称为MDL锁,这是根据表的元数据加锁,什么意思呢?我们到现在停止现已模模糊糊懂得一个概念:表锁是根据整张表加锁,行锁是根据一条数据加锁,那这个表的元数据是什么东东呢?在《索引原理篇》中聊索引的完结时,曾提到过一点:全部存储引擎的表都会存在一个.frm文件,这个文件中首要存储表的结构(DDL句子),而DML锁便是根据.frm文件中的元数据加锁的。

关于这种锁是在MySQL5.5版别后再开端支撑的,一般来说我们用不上,因而也无需手动获取锁,这个锁首要是用于:更改表结构时运用,比方你要向一张表创立/删去一个索引、修正一个字段的称号/数据类型、增加/删去一个表字段等这类状况。

因为究竟当你的表结构正在产生更改,假定此刻有其他业务来对表做CRUD操作,天然就会呈现问题,比方我刚删了一个表字段,成果另一个业务中又按本来的表结构刺进了一条数据,这明显会存在危险,因而DML锁在加锁后,整张表不答应其他业务做任何操作。

3.2、意向锁(Intention Lock)

前面提到过,InnoDB引擎是一种支撑多粒度锁的引擎,而意向锁则是InnoDB中为了支撑多粒度的锁,为了兼容行锁、表锁而规划的,怎样了解这句话呢?先来看一个比方:

假定一张表中有一千万条数据,现在业务T1ID=8888888的这条数据加了一个行锁,此刻来了一个业务T2,想要获取这张表的表等级写锁,经过前面的一系列解说,我们应该知道写锁有必要为排他锁,也便是在同一时间内,只答应当时业务操作,假如表中存在其他业务现已获取了锁,现在业务就无法满意“独占性”,因而不能获取锁。

那考虑一下,因为T1是对ID=8888888的数据加了行锁,那T2获取表锁时,是不是得先判别一下表中是否存在其他业务在操作?但因为InnoDB中有行锁的概念,所以表中任何一行数据上都有或许存在业务加锁操作,为了能精准的知道答案,MySQL就得将整张表的1000W条数据全部遍历一次,然后逐条检查是否有锁存在,那这个功率天然会非常的低。

有人或许会说,慢就慢点怎样了,能接受!但实际上不仅仅存在这个问题,还有别的一个丧命问题,比方现在MySQL现已判别到了第567W行数据,发现前面的数据上都没有锁存在,正在持续往下遍历。

要记住MySQL是支撑并发业务的,也便是MySQL正在扫描后边的每行数据是否存在锁时,万一又来了一个业务在扫描过的数据行上加了个锁怎样办?比方在第123W条数据上加了一个行锁。那难道又从头扫描一遍嘛?这就堕入了死循环,行锁和表锁之间呈现了兼容问题。

也正是因为行锁和表锁之间存在兼容性问题,所以意向锁它来了!意向锁实际上也是一种特别的表锁,意向锁其实是一种“挂牌奉告”的思维,比方日常日子中的出租车,一般都会有一个牌子,表明它现在是“空车”仍是“载客”状况,而意向锁也是这个思维。

比方当业务T1打算对ID=8888888这条数据加一个行锁之前,就会先加一个表等级的意向锁,比方现在T1要加一个行等级的读锁,就会先增加一个表等级的意向同享锁,假如T1要加行等级的写锁,亦是同理。

此刻当业务T2测验获取一个表级锁时,就会先看一下表上是否有意向锁,假如有的话再判别一下与自身是否抵触,比方表上存在一个意向同享锁,现在T2要获取的是表等级的读锁,那天然不抵触可以获取。但反之,假如T2要获取一个表记的写锁时,就会呈现抵触,T2业务则会堕入堵塞,直至T1开释了锁(业务完毕)停止。

3.3、自增锁(AUTO-INC Lock

自增锁,这个是专门为了提高自增ID的并发刺进功用而规划的,一般状况下我们在建表时,都会对一张表的主键设置自增特性,如下:

CREATE TABLE `table_name` (
    `xx_id` NOT NULL AUTO_INCREMENT,
    .....
) ENGINE = InnoDB;

当对一个字段设置AUTO_INCREMENT自增后,意味着后续刺进数据时无需为其赋值,系统会主动赋上次序自增的值。但想一想,比方现在表中最大的ID=88,假如两个并发业务一起对表履行刺进句子,因为是并发履行的原因,所以有或许会导致刺进两条ID=89的数据。因而这儿有必要要加上一个排他锁,保证并发刺进时的安全性,但也因为锁的原因,刺进的功率也就因而降低了,究竟将全部写操作串行化了。

为了改进刺进数据时的功用,自增锁诞生了,自增锁也是一种特别的表锁,但它仅为具有AUTO_INCREMENT自增字段的表服务,一起自增锁也分成了不同的等级,可以经过innodb_autoinc_lock_mode参数操控。

  • innodb_autoinc_lock_mode = 0:传统方法。
  • innodb_autoinc_lock_mode = 1:接连方法(MySQL8.0以前的默许方法)。
  • innodb_autoinc_lock_mode = 2:交织方法(MySQL8.0之后的默许方法)。

当然,这三种方法又是什么意义呢?想要彻底搞清楚,那就得先弄理解MySQL中或许呈现的三种刺进类型:

  • 一般刺进:指经过INSERT INTO table_name(...) VALUES(...)这种方法刺进。
  • 批量刺进:指经过INSERT ... SELECT ...这种方法批量刺进查询出的数据。
  • 混合刺进:指经过INSERT INTO table_name(id,...) VALUES(1,...),(NULL,...),(3,...)这种方法刺进,其间一部分指定ID,一部分不指定。

简略了解上述三种刺进方法后,再用一句话来概述自增锁的作用:自增锁首要负责维护并发业务下自增列的次序,也便是说,每当一个业务想向表中刺进数据时,都要先获取自增锁先分配一个自增的次序值,但不同方法下的自增锁也会有少许不同。

传统方法:业务T1获取自增锁刺进数据,业务T2也要刺进数据,此刻业务T2只能堵塞等候,也便是传统方法下的自增锁,一起只答应一条线程履行,这种方法明显功用较低。

接连方法:这个方法首要是因为传统方法存在功用短板而研发的,在这种方法中,关于可以提早确认数量的刺进句子,则不会再获取自增锁,啥意思呢?也便是关于“一般刺进类型”的句子,因为在刺进之前就现已确认了要刺进多少条数据,因为会直接分配规模自增值。

比方现在业务T1要经过INSERT INTO...句子刺进十条数据,现在表中存在的最大ID=88,那在接连方法下,MySQL会直接将89~98这十个自增值分配给T1,因而T1无需再获取自增锁,但不获取自增锁不代表不获取锁了,而是改为运用一种轻量级锁Mutex-Lock来避免自增值重复分配。

关于一般刺进类型的操作,因为可以提早确认刺进的数据量,因而可以采用“预分配”思维,但如若关于批量刺进类型的操作,因为批量刺进的数据是根据SELECT句子查询出来的,所以在履行之前也无法确认究竟要刺进多少条,所以依旧会获取自增锁履行。也包括关于混合刺进类型的操作,有一部分指定了自增值,但有一部分需求MySQL分配,因而“预分配”的思维也用不上,因而也要获取自增锁履行。

交织方法:在交织刺进方法中,关于INSERT、REPLACE、INSERT…SELECT、REPLACE…SELECT、LOAD DATA等一系列刺进句子,都不会再运用表等级的自增锁,而是全都运用Mutex-Lock来保证安全性,为什么在这个方法中,批量刺进也可以不获取自增锁呢?这跟它的名字有关,现在这个方法叫做交织刺进方法,也便是不同业务之间刺进数据时,自增列的值是交织刺进的,举个比方了解。

比方业务T1、T2都要履行批量刺进的操作,因为不确认各自要刺进多少条数据,所以之前那种“接连预分配”的思维用不了了,但尽管无法做“接连的预分配”,那能不能交织预分配呢?比方给T1分配{1、3、5、7、9....},给T2分配{2、4、6、8、10.....},然后两个业务交织刺进,这样岂不是做到了自增值即不重复,也能支撑并发批量刺进?答案是Yes,但因为两个业务履行的都是批量刺进的操作,因而事先不确认刺进行数,所以有或许导致“交织预分配”的次序值,有或许不会运用,比方T1只刺进了四条数据,只用了1、3、5、7T2刺进了五条数据,因而表中的自增值有或许呈现空地,即{1、2、3、4、5、6、8、10},其间9就并未运用。

尽管我没看过自增锁这块的源码,但交织刺进方法底层应该是我估测的这种方法完结的,也便是利用自增列的步长机制完结,不过因为刺进或许会呈现空地,因而对后续的主从复制也有一定影响(今后再细聊)。

不过相对来说影响也不大,尽管无法保证自增值的接连性,但至少能保证递增性,因而对索引的维护不会形成额定开支

3.4、大局锁

大局锁其实是一种尤为特别的表锁,其实将它称之为库锁或许更适宜,因为大局锁是根据整个数据库来加锁的,加上大局锁之后,整个数据库只能答应读,不答应做任何写操作,一般大局锁是在对整库做数据备份时运用。

-- 获取大局锁的指令
FLUSH TABLES WITH READ LOCK;
-- 开释大局锁的指令
UNLOCK TABLES;

从上述的指令也可以看出,为何将其归纳到表锁规模,因为获取锁以及开释锁的指令都是表锁的指令。

四、MySQL行锁

一般而言,为了尽或许提高数据库的整体功用,所以每次在加锁时,锁的规模天然是越小越好,举个比方:

假定此刻有1000个恳求,要操作zz_users表中的数据,假如以表粒度来加锁,假定第一个恳求获取到的是排他锁,也就意味着其他999个恳求都需求堵塞等候,其功率可想而知….

仔细一考虑:尽管此刻有1000个恳求操作zz_users表,但这些恳求中至少90%以上,要操作的都是表中不同的行数据,因而如若每个恳求都获取表级锁,明显太影响功率了,而InnoDB引擎中也考虑到了这个问题,所以完结了更细粒度的锁,即行锁

4.1、表锁与行锁之间的联系

表锁与行锁之间的联系,举个日子中的比方来快速了解一下,一张表就相似于一个日子中的酒店,每个业务/恳求就相似于一个个旅客,旅客住宿为了保证夜晚安全,一般都会锁门保护自己。而表锁呢,就相似于一个旅客住进酒店之后,直接把酒店大门给锁了,其他旅客就只能等第一位旅客住完出来之后才干一个个进去,每个旅客进酒店之后的第一件事情便是锁大门,避免其他旅客要挟自己的安全问题。

但假定酒店门口来了一百位旅客,其间大部分旅客都是不同的房间(情侣除外),因而直接锁酒店大门明显并不合理。

而行锁呢,就相似于房间的锁,门口的100位旅客可以一起进酒店,每位旅客住进自己的房间之后,将房门反锁,这明显也能保证各自的人身安全问题,一起也能让一个酒店在同一时间内接收更多的旅客,“功用”更高。

4.2、InnoDB的行锁完结

放眼望去,在MySQL诸多的存储引擎中,仅有InnoDB引擎支撑行锁(不考虑那些闭源自研的),这是因为什么原因导致的呢?因为InnoDB支撑聚簇索引,在之前简略聊到过,InnoDB中假如可以射中索引数据,就会加行锁,无法射中则会加表锁。

在《索引原理篇-InnoDB索引完结》中提到过,InnoDB会将表数据存储在聚簇索引中,每条行数据都会存储在树中的叶子节点上,因而行数据是“分隔的”,所以可以对每一条数据上锁,但其他引擎大部分都不支撑聚簇索引,表数据都是一起存储在一块的,所以只能根据整个表数据上锁,这也是为什么其他引擎不支撑行锁的原因。

4.3、记载锁(Record Lock)

Record Lock记载锁,实际上便是行锁,一行表数据、一条表记载自身便是同一个意义,因而行锁也被称为记载锁,两个称号终究指向的是同一类型的锁,那如何运用行锁呢?

-- 获取行等级的 同享锁
select * from `zz_users` where user_id = 1 lock in share mode;
-- 获取行等级的 排他锁
select * from `zz_users` where user_id = 1 for update;

是的,你没看错,想要运用InnoDB的行锁便是这样写的,假如你的SQL能射中索引数据,那也就天然加的便是行锁,反之则是表锁。但网上许多材料都流传着一个说法:InnoDB引擎的表锁没啥用,其实这句话会存在少许误导性,因为意向锁、自增锁、DML锁都是表锁,也包括InnoDB的行锁是根据索引完结的,例如在update句子修正数据时,假定where后边的条件无法射中索引,那咋加行锁呢?此刻没办法就有必要得加表锁了,因而InnoDB的表锁是有用的。

4.4、空地锁(Gap Lock)

空地锁是对行锁的一种补充,首要是用来处理幻读问题的,但想要了解它,我们首要来了解啥叫空地:

SELECT * FROM `zz_users`;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
|       2 | 竹子      || 1234     | 2022-09-14 16:17:44 |
|       3 | 子竹      || 4321     | 2022-09-16 07:42:21 |
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+

上述这张表终究两条数据,ID字段之间从4跳到了9,那么4~9两者之间的规模则被称为“空地”,而空地锁则首要确定的是这块规模。

那为何又说空地锁是用来处理幻读问题的呢?因为幻读的概念是:一个业务在履行时,另一个业务刺进了一条数据,然后导致第一个业务操作完结之后发现成果与预想的不一致,跟产生了幻觉相同。
比方拿上述表举比方,现在要将ID>3的用户暗码重置为1234,因而业务T1先查到了ID>34、9两条数据并上锁了,然后开端更改用户暗码,但此刻业务T2过来又刺进了一条ID=6、password=7777的数据并提交,等T1修正完了4、9两条数据后,此刻再次查询ID>3的数据时,成果发现了ID=6的这条数据并未被重置暗码。

在上述这个比方中,T2因为新增并提交了业务,所以T1再次查询时也能看到ID=6的这条数据,就跟产生了幻觉似的,关于这种新增数据,专业的叫法称之为幻影数据。

为了避免呈现安全问题,所以T1在操作之前会对方针数据加锁,但在T1业务履行时,这条幻影数据还不存在,因而就会呈现一个新的问题:不知道把锁加在哪儿,究竟想要对ID=6的数据加锁,便是加了个孤寂。

那难道不加锁了吗?肯定得加锁,但怎样加呢?一般的行锁就现已无法处理这个问题了,总不能加表锁吧,那也太影响功用了,所以空地锁应运而生!空地锁的功用与它的名字相同,首要是对空地区域加锁,举个比方:

select * from `zz_users` where user_id = 6 lock in share mode;

这条加锁的SQL看起来似乎不是那么合理对吧?究竟ID=6的数据在表中还没有呀,咋加锁呢?其实这个便是空地锁,此刻会确定{4~9}之间、但不包括4、9的区域,因为空地锁是遵从左右开区间的原则,简略演示一下事例:

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

上述事例的进程参考图中注释即可,不再反复赘述,简略说一下定论:当对一个不存在的数据加锁后,默许便是确定前后两条数据之间的区间,当其他业务再测验向该区间刺进数据时,就会堕入堵塞,只有当持有空地锁的业务完毕后,才干持续履行刺进操作。

不过空地锁加在不同的方位,确定的规模也并不相同,假如加在两条数据之间,那么确定的区域便是两条数据之间的空地。假如加在上表ID=1的数据上,确定的区域便是{-∞ ~ 1},即无量小到1之间的区域。假如加在ID=9之后,确定的区域便是{9 ~ +∞},即9之后到无量大的区域。

4.5、临键锁(Next-Key Lock)

临键锁是空地锁的Plus版别,或者可以说成是一种由记载锁+空地锁组成的锁:

  • 记载锁:确定的规模是表中具体的一条行数据。
  • 空地锁:确定的规模是左闭右开的区间,并不包括终究一条真实数据。

而临键锁则是两者的结合体,加锁后,即确定左闭右开的区间,也会确定当时行数据,仍是以上述表为例,做个简略的小试验,如下:

(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?

这回和空地锁的试验相似,但也并不相同,这回是根据表中ID=9的这条数据加锁的,此刻来看成果,除开确定了4~9这个区间外,关于ID=9这条数据也确定了,因为在业务T2中测验对ID=9的数据修正时,也会让业务堕入堵塞。

临键锁的留意点:当本来持有锁的T1业务完毕后,T2会履行刺进操作,这时锁会被T2获取,当你再测验敞开一个新的业务T3,再次获取相同的临键锁时,是无法获取的,只能等T2完毕后才干获取(因为临建锁包括了记载锁,尽管空地锁可以一起由多个业务持有,但排他类型的记载锁只答应一个业务持有)。

实际上在InnoDB中,除开一些特别状况外,当测验对一条数据加锁时,默许加的是临键锁,而并非记载锁、空地锁。

也便是说,在前面举例幻读问题中,当T1要对ID>3的用户做暗码重置,确定4、9这两条行数据时,默许会加的是临键锁,也便是当业务T2测验刺进ID=6的数据时,因为有临建锁存在,因而无法再刺进这条“幻影数据”,也就至少保证了T1业务履行进程中,不会碰到幻读问题。

4.6、刺进意向锁(Insert Intention Lock)

刺进意向锁,听起来似乎跟前面的表等级意向锁有些相似,但实际上刺进意向锁是一种空地锁,这种锁是一种隐式锁,也便是我们无法手动的获取这种锁。一般在MySQL中刺进数据时,是并不会产生锁的,但在刺进前会先简略的判别一下,当时业务要刺进的方位有没有存在空地锁或临键锁,假如存在的话,当时刺进数据的业务则需堵塞等候,直到拥有临键锁的业务提交。

当业务履行刺进句子堵塞时,就会生成一个刺进意向锁,表明当时业务想对一个区间刺进数据(现在的业务处于等候刺进意向锁的状况)。

当持有本来持有临建锁的业务提交后,当时业务即可以获取刺进意向锁,然后履行刺进操作,当此刻如若又来一个新的业务,也要在该区间中刺进数据,那新的业务会堵塞吗?答案是不会,可以直接履行刺进操作,Why

因为在之前的《SQL履行篇-写入SQL履行流程》中曾说到过,关于写入SQL都会做一次唯一性检测,假如要刺进的数据,与表中已有的数据,存在唯一性抵触时会直接抛出反常并返回。这也就意味着:假如没抛出反常,就代表着当时要刺进的数据与表中数据不存在唯一性抵触,或表中压根不存在唯一性字段,可以答应刺进重复的数据。

简略来说便是:可以真实履行的刺进句子,肯定是经过了唯一检测的,因而刺进时可以让多业务并发履行,一起假如设置了自增ID,也会获取自增锁保证安全性,所以当多个业务要向一个区间刺进数据时,刺进意向锁是不会排挤其他业务的,从这种角度而言,刺进意向锁也是一种同享锁。

4.7、行锁的粒度粗化

有一点要值得留意:行锁并不是原封不动的,行锁会在某些特别状况下产生粗化,首要有两种状况:

  • 在内存中专门分配了一块空间存储锁方针,当该区域满了后,就会将行锁粗化为表锁。
  • 作为规模性写操作时,因为要加的行锁较多,此刻行锁开支会较大,也会粗化成表锁。

当然,这两种状况其实很少见,因而只需求知道有锁粗化这回事即可,这种锁粗化的现象其实在SQLServer数据库中更常见,因为SQLServer中的锁机制是根据行记载完结的,而MySQL中的锁机制则是根据业务完结的(后续《业务与锁原理篇》具体剖析)。

五、页面锁、达观锁与失望锁

上述对MySQL两种较为常见的锁粒度进行了论述,接着再来看看页面锁、达观锁与失望锁。

5.1、页面锁

页面锁是Berkeley DB存储引擎支撑的一种锁粒度,当然,因为BDB引擎被Oracle收买的原因,因而MySQL5.1今后不再直接性的支撑该引擎(需自己整合),因而页锁见的也比较少,我们略微了解即可。

  • 表锁:以表为粒度,锁住的是整个表数据。
  • 行锁:以行为粒度,锁住的是一条数据。
  • 页锁:以页为粒度,锁住的是一页数据。

唯一有少许疑惑的当地,便是一页数据究竟是多少呢?其实我也不大清楚,究竟没用过BDB引擎,但我估量便是只一个索引页的巨细,即16KB左右。

简略了解后页锁后,接着来看一看从思维维度区分的两种锁,即达观锁与失望锁。

5.2、达观锁

达观锁即是无锁思维,关于这点在之前聊《并发编程系列-Unsafe与原子包》时曾具体讲到过,但失望锁也好,达观锁也罢,实际上仅是一种锁的思维,如下:

  • 达观锁:每次履行都以为只会有自身一条线程操作,因而无需拿锁直接履行。
  • 失望锁:每次履行都以为会有其他线程一起来操作,因而每次都需求先拿锁再履行。

达观锁与失望锁也对应着我们日常日子中,处理一件事情的态度,一个人性情很达观时,做一件事情时都会把成果往好处想,而一个人性情很失望时,处理一件事情都会做好最坏的打算。

OK~,编程中的无锁技能,或者说达观锁机制,一般都是根据CAS思维完结的,而在MySQL中则可以经过version版别号+CAS的方法完结达观锁,也便是在表中多规划一个version字段,然后在SQL修正时以如下方法操作:

UPDATE ... SET version = version + 1 ... WHERE ... AND version = version;

也便是每条修正的SQL都在修正后,对version字段加一,比方T1、T2两个业务一起并发履行时,当T2业务履行成功提交后,就会对version+1,因而业务T1version=version这个条件就无法成立,终究会抛弃履行,因为现已被其他业务修正过了。

当然,一般的达观锁都会配合轮询重试机制,比方上述T1履行失利后,再次履行相同句子,直到成功停止。

从上述进程中不难看出,这个进程中确实未曾增加锁,因而也做到了达观锁/无锁的概念落地,但这种方法却并不合适全部状况,比方写操作的并发较高时,就简略导致一个业务长时间一直在重试履行,然后导致客户端的响应尤为缓慢。

因而达观锁愈加适用于读大于写的业务场景,频繁写库的业务则并不合适加达观锁。

5.3、失望锁

失望锁的思维我们上面现已提到了,即每次履行时都会加锁再履行,我们之前剖析的《synchronized关键字》、《AQS-ReetrantLock》都属于失望锁类型,也便是在每次履行前有必要获取到锁,然后才干持续往下履行,而数据库中的排他锁,便是一种典型的失望锁类型。

在数据库中想要运用失望锁,那也便是对一个业务加排他锁for update即可,不再重复赘述。

五、MySQL锁机制总结

看到这儿,信任我们对MySQL中供给的锁机制有了全面的知道,但以现在状况而言,虽对每种锁类型有了基本认知,但本篇的内容更像一个个的点,很难和《MySQL业务篇》连成线,而关于这块的具体内容,则会放在后续的《业务与锁机制的完结原理篇》中具体解说,在后续的原理篇中再将这一个个知识点串联起来,因为想要真实弄懂MySQL业务阻隔机制的完结,还缺少了一块至关重要的点没讲到:即MVCC机制。

因而会先讲理解数据库的MVCC多版别并发操控技能的完结后,再去剖析业务阻隔机制的完结。

终究再来简略的总结一下本篇所聊到的不同锁,它们之间的抵触与兼容联系:

PS:表中横向(行)表明现已持有锁的业务,纵向(列)表明正在恳求锁的业务。

行级锁比照 同享临键锁 排他临键锁 空地锁 刺进意向锁
同享临键锁 兼容 抵触 兼容 抵触
排他临键锁 抵触 抵触 兼容 抵触
空地锁 兼容 兼容 兼容 抵触
刺进意向锁 抵触 抵触 抵触 兼容

因为临建锁也会确定相应的行数据,因而上表中也不再重复赘述记载锁,临建锁兼容的 记载锁都兼容,同理,抵触的记载锁也会抵触,再来看看标记别的锁比照:

表级锁比照 同享意向锁 排他意向锁 元数据锁 自增锁 大局锁
同享意向锁 兼容 抵触 抵触 抵触 抵触
排他意向锁 抵触 抵触 抵触 抵触 抵触
元数据锁 抵触 抵触 抵触 抵触 抵触
自增锁 抵触 抵触 抵触 抵触 抵触
大局锁 兼容 抵触 抵触 抵触 抵触

放眼望下来,其实会发现表等级的锁,会有许多许多抵触,因为锁的粒度比较大,因而许多时分都会呈现抵触,但关于表级锁,我们只需求重视同享意向锁和同享排他锁即可,其他的大多数为MySQL的隐式锁(在这儿,同享意向锁和排他意向锁,也可以了解为MyISAM中的表读锁和表写锁)。

终究再简略的说一下,表中的抵触和兼容究竟是啥意思?抵触的意思是当一个业务T1持有某个锁时,另一个业务T2来恳求相同的锁,T2会因为锁排挤会堕入堵塞等候状况。反之同理,兼容的意思是指答应多个业务一起获取同一个锁。

MySQL5.7版别中新增的同享排他锁

关于这条是终究补齐的,之前漏写了这种锁类型,在MySQL5.7之前的版别中,数据库中仅存在两种类型的锁,即同享锁与排他锁,可是在MySQL5.7.2版别中引进了一种新的锁,被称之为(SX)同享排他锁,这种锁是同享锁与排他锁的杂交类型,至于为何引进这种锁呢?聊它之前需求先了解SMO问题:

SQL履行期间一旦更新操作触发B+Tree叶子节点割裂,那么就会对整棵B+Tree加排它锁,这不光堵塞了后续这张表上的全部的更新操作,一起也阻止了全部试图在B+Tree上的读操作,也便是会导致全部的读写操作都被堵塞,其影响巨大。因而,这种大粒度的排它锁成为了InnoDB支撑高并发访问的首要瓶颈,而这也是MySQL 5.7版别中引进SX锁要处理的问题。

那想一下该如何处理这个问题呢?最简略的方法便是减小SMO问题产生时,确定的B+Tree粒度,当产生SMO问题时,就只确定B+Tree的某个分支,而并不是确定整颗B+树,然后做到不影响其他分支上的读写操作。

MySQL5.7中引进同享排他锁后,究竟是如何完结的这点呢?首要要弄清楚SX锁的特性,它不会堵塞S锁,可是会堵塞X、SX锁,下面展开来聊一聊。

在聊之前首要得搞清楚SQL履行时的几个概念:

  • 读取操作:根据B+Tree去读取某条或多条行记载。
  • 达观写入:不会改动B+Tree的索引键,仅会更改索引值,比方主键索引树中不修正主键字段,只修正其他字段的数据,不会引起节点割裂。
  • 失望写入:会改动B+Tree的结构,也便是会形成节点割裂,比方无序刺进、修正索引键的字段值。

MySQL5.6版别中,一旦有操作导致了树结构产生变化,就会对整棵树加上排他锁,堵塞全部的读写操作,而MySQL5.7版别中,为了处理该问题,关于不同的SQL履行,流程就做了调整。

MySQL5.7中读操作的履行流程

  • ①读取数据之前首要会对B+Tree加一个同享锁。
  • ②在根据树检索数据的进程中,关于全部走过的叶节点会加一个同享锁。
  • ③找到需求读取的方针叶子节点后,先加一个同享锁,开释过程②上加的全部同享锁。
  • ④读取终究的方针叶子节点中的数据,读取完结后开释对应叶子节点上的同享锁。

MySQL5.7中达观写入的履行流程

  • ①达观写入之前首要会对B+Tree加一个同享锁。
  • ②在根据树检索修正方位的进程中,关于全部走过的叶节点会加一个同享锁。
  • ③找到需求写入数据的方针叶子节点后,先加一个排他锁,开释过程②上加的全部同享锁。
  • ④修正方针叶子节点中的数据后,开释对应叶子节点上的排他锁。

MySQL5.7中失望写入的履行流程

  • ①失望更新之前首要会对B+Tree加一个同享排他锁。
  • ②因为①上现已加了SX锁,因而当时业务履行进程中会堵塞其他测验更改树结构的业务。
  • ③遍历查找需求写入数据的方针叶子节点,找到后对其分支加上排他锁,开释①中加的SX锁。
  • ④履行SMO操作,也便是履行失望写入操作,完结后开释过程③中在分支上加的排他锁。

假如需求修正多个数据时,会在遍历查找的进程中,记载下全部要修正的方针节点。

MySQL5.7中并发业务抵触剖析

观察上述讲到的三种履行状况,关于读操作、达观写入操作而言,并不会加SX锁,同享排他锁仅针关于失望写入操作会加,因为读操作、达观写入履行前对整颗树加的是S锁,因而失望写入时加的SX锁并不会堵塞达观写入和读操作,但当另一个业务测验履行SMO操作改变树结构时,也需求先对树加上一个SX锁,这时两个失望写入的并发业务就会呈现抵触,新来的业务会被堵塞。

可是要留意:当第一个业务寻找到要修正的节点后,会对其分支加上X锁,紧接着会开释B+Tree上的SX锁,这时别的一个履行SMO操作的业务就能获取SX锁啦!

其实从上述中或许得知一点:MySQL5.7版别引进同享排他锁之后,处理了5.6版别产生SMO操作时堵塞全部读写操作的问题,这样可以在一定程度上提高了InnoDB表的并发功用。

终究要留意:尽管一个履行失望写入的业务,找到了要更新/刺进数据的节点后会开释SX锁,可是会对其上级的叶节点(叶分支)加上排他锁,因而正在产生SMO操作的叶分支,依旧是会堵塞全部的读写行为!

上面这句话啥意思呢?也便是当一个要读取的数据,位于正在履行SMO操作的叶分支中时,依旧会被堵塞。