感受数据库有不少说烂了的问题,在实际应用中总仍是容易出问题。正好就一个真实的踩坑场景来讨论一下这个话题。一次踩坑每每是咱们理解一个问题的开始,这个想法是《踩坑攀登者》(pitfall climbers)系列文章的开始。java
为了把握篇幅(在5000字之内)文章分红上下两篇,本文主要讲两种reading和innodb的各类锁的设计。mysql
本文以这个table做为例子来进行讲解,注意例子中有1个汇集索引2个次级索引:redis
一个链接但愿修改用户555的全部的数据而且不容许有其余的新数据产生。sql
select * from tb_complaint_user where user_id = 555 for update; # 写锁
--- 修改用户555的全部数据
commit;
复制代码
结果大失所望,另外一个线程成功的insert了新的用户555的数据,但另外一个线程的update 555的语句却被block住了。数据库
经验丰富的同窗立刻发现了问题来自隔离级别,查看了隔离级别发现级别REAT-COMMITED。(注意auto_commit都是false)因而尝试在测试环境把隔离级别改为REPEATABLE-READ,再次重试insert就被成功的block了。(你们注意不要在生产环境随便修改配置- -)bash
这里面整个过程到底发生了什么呢?session
阅读锁须要先了解innodb是基于transaction和索引的:1)在innodb中仅select也在一个transaction里,不是只有update这种语句才会开启transaction;2)innodb是基于索引的设计,行数据存在于汇集索引(clustered-index)的叶子节点上(leaf-records);多线程
另,若是没有明确指定,例子里的隔离级别都是read-committed,auto_commit = false;测试
首先先看看reads的一种分类方法,innodb的官方文档根据reads是否须要请求数据库锁将其分为locking reads和none-locking reads。ui
none-locking reads是不加锁的查询,也就是咱们最经常使用的select。在普通的select语句下,咱们的transaction不会申请任何锁从而也不会被任何锁block。
select * from tb_xxxx [where xxxx] [group by xxx] [having xxx] [order by xxx]
复制代码
那么locking reads就很好理解了,须要请求数据库锁的就是locking reads了,请求锁也就极可能须要等锁被block。下面几种reading语句都属于locking reads。
select xxx from tb_xxx for share;
select xxx from tb_xxx for update;
update xxx set xxx = xxx where xxx = xxx; //因为直接调用update/delete其实也会申请和for update同样的锁,
delete from xxx where xxx;
复制代码
首先说说什么是锁,锁的设计其实很是多不只仅是db会用到,redis、多线程开发中都会用到,并且业务开发的时候也经常用到锁的设计:好比参加双十一的商品不能再下架,这个时候淘宝系统就能够对这个商品加一个业务上的商品锁。
那么锁是什么呢,从实现角度看极可能只是一个行tag,标记了锁的类型、锁的对象、锁的拥有者等等。从含义理解锁是一种用来限制某种资源的某一能力被使用的机制,这个角度进而带给咱们几个理解锁的角度:锁什么资源、锁限制了别人的什么能力、锁赋予了拥有者什么能力、多个锁是否共存。
innodb对锁的第一种分类方式就是根据“锁出现的原因”来分,对metaData好比table,procedure,db的定义而发生改动的锁叫metaData lock,因为对具体数据改动或者限制产生的锁称为datalock。好比DDL语句就须要锁住table定义从而申请的是table meta lock,而修改一个table中的一行申请的的锁就是datalock。
datalock每每是基于transaction的,结束占有锁的trx就会释放(rollback或者commit),固然和db的这个session直接结束了trx也会结束从而datalock释放。
metadata lock每每不是基于transaction而是基于session的,因此metadata的lock没法经过结束当前trx来结束,unlock须要显示调用、或者等申请锁的DDL执行完毕自动解锁、再或者直接close session- -。
LOCK TABLES table_name [READ | WRITE]
UNLOCK TABLES;
复制代码
查看innodb中正在使用的datalock和metalock都在performance_schema这个db中(mysql中schema和db属于等价概念),使用下面语句就能够查看当前正在使用中的locks了。固然直接查看innodb status也能看到锁的具体信息。
select * from performance_schema.metadata_locks;
select * from performance_schema.data_locks;
show engine innodb status;
复制代码
这一部分讲的互斥锁(X=exclusive)和共享锁(S=shared)更多的是讲互斥和共享的设计,而不是具体的一种数据库锁,这种共享和互斥关系针对具体的资源不管一个行记录(record)仍是表(table)都适用。
S锁正如其名shared能够被多个trx(T1,T2,T3均可以得到),获取S锁表明该资源(record/table)正在被很”认真“的读,限制的是在这个读的过程当中不能发生修改删除(dml)操做。若是java的GC回收认为没法回收的对象是被root节点锁住了,那么这种锁就是一种shared锁,多个roots能够共享限制了回收能力。
X锁exclusive也就是排它互斥的意思,同一时刻只能被一个trx来得到,下一个trx申请锁每每会被block直至锁被释放,得到X锁认为该资源正在被一个trx修改从而不能被S锁加锁读也不能被其余线程加X锁写。java中的sync和lock都是一种排他锁,限制了对象代码的执行权限。
根据行锁锁定的范围,row lock这一个笼统的概念能够再细粒分为record lock,insert-lock, gap lock,next-key lock。next-key实际上是record和gap的组合,因此重点在于理解recard,insert和gap lock。
record lock应该具叫index record lock才更准确。这种锁的目标就是命中sql条件的一个个index records, 这里的index最重要的是执行计划使用到的key,此外也会在pk上加记录锁。因此一个命中记录极可能会加多个记录锁
另外一个经常误解的知识点事index record的粒度是“一个个”。什么叫一个呢,若是使用的key不是unique key,这一行的定位应该是(key=xxx,pk=xxx)(也就是次级索引叶子节点里的内容:key+pk)。这里不能笼统的理解为对(key=xxx)加了锁由于key=xxx是一个多行表明的是从xxx到xxx+1的这个范围。
记录锁在innodb系统中的标识LockMode注意不是X,S而是(X/S,LOCK_REC_NOT_GAP),也就是lock is record not gap的意思。
下面咱们看看例子理解一下上面的设定,如今咱们user_complaint表上有2行数据:
咱们经过userid和名字来select,explain sql发现执行计划使用userid做为索引。查看data_locks表里的recordMode和indexName,发现如同上文所说的innodb在pk和userid上使用了recordLock。查看lockData这一列的数据发现user_id这个次级索引上的record(userId=555, id=1)和pk这个unique的汇集索引上的记录是(id=1)。
explain select * from tb_user_complaint where user_id = 555 and user_name = "macavity" for update;
select * from tb_user_complaint where user_id = 555 and user_name = "macavity" for update;
select * from performance_schema.data_locks;
rollback;
复制代码
若是把select中的userid条件删掉会发现recordlock加载了pk和userName这两个index上,由于这个时候执行计划使用的key就是userName了; 若是把select的条件改为id=1那么执行计划用的index就是pk也就只会有pk这一个index lock。
当record与X和S的机制结合在一块儿时,就是咱们最常说的行同享排它锁。能够获得了如下互斥图。注意,这个互斥图中的X与S是指同一行的行锁,不要理解成其余行更不要表的x与s和行的x与s混淆在这张图里。
DML中insert操做和update、delete不太相同,udpate和delete是对已经有的数据(index records)进行操做,而insert操做(update更改index的操做实际上是另外一种update+insert)是在间隙中新增index records。因此insert操做涉及到的锁再也不是record lock而是insert intention lock。
因为insert的行还未存在(命中unique key duplicate error的除外),insert锁是没法被record锁排斥的。这也就是案例中的问题。为了防止insert锁,innodb设计了一种转么针对不存在锁
gaplock,顾名思义它再也不锁定一个个的index record,而是锁定一个records之间的空隙。间隙锁的lockmode为(X/S,Gap)。这里必定注意间隙锁表示的是空隙,并不表示构成间隙的边界(前提是边界上本身没有加行锁),因此gap lock不会与表示实体行的record lock互斥而会对一样限制间隙的insert lock互斥。
innodb只有在isolation>=repeatble-read(简称RR)的隔离级别才会采用间隙锁,因此咱们这里须要把修改隔离级别。 隔离级别和锁的关系会在下一篇详细介绍,本篇的目的是理解锁。
咱们再次执行下面sql。
set @@session.transaction_isolation='repeatable-read';
commit;
select * from tb_user_complaint where user_id = 555 for update;
复制代码
当隔离级别变成RR以后咱们再次执行加锁语句,发现多了一行gaplock。gaplock标记着gap的结束点在(userId=580,id=36)的位置,也就是(555,16)以后的第一个record。这个时候(555, 16)到(580, 36)这个区间都没法再insert任何数据。
+------+----------------------------+---------+------------------------+------------+----------------------------+-----------+
| id | created_at | user_id | contents | is_archive | last_updated_at | user_name |
+------+----------------------------+---------+------------------------+------------+----------------------------+-----------+
| 1 | 2020-02-12 15:12:11.922491 | 555 | complaint-test-1 | ^@ | 2020-02-12 20:12:44.989804 | macavity |
| 2 | 2020-02-12 15:12:19.214543 | 222 | complaint-test-1 | ^@ | 2020-02-12 15:12:19.214543 | macavity |
| 10 | 2020-02-12 15:26:50.164283 | 111 | complaint-test-1 | ^@ | 2020-02-12 15:26:50.164283 | macavity |
| 16 | 2020-02-12 15:42:58.989849 | 555 | complaint-typed-by Bob | ^@ | 2020-02-12 15:42:58.989849 | macavity |
| 36 | 2020-02-12 18:03:55.962341 | 580 | complaint-test-1 | ^@ | 2020-02-12 18:03:55.962341 | macavity |
| 1012 | 2020-02-12 20:52:51.732739 | 600 | complaint-test-1 | ^@ | 2020-02-12 20:52:51.732739 | kiki |
+------+----------------------------+---------+------------------------+------------+----------------------------+-----------+
复制代码
这里须要注意的是(X,Gap)表明间隙锁并不表明记录锁,也就是(580,36)这行数据并无被行锁锁定。select for update这行仍是能够得到这行的记录锁也就是(X, REC_NOT_GAP)锁。
下面咱们看一下间隙锁和insert lock的同享互斥关系:
在上文间隙锁的案例里,当咱们改变隔离级别,gap lock中除了红色的X,GAP以外的变动,其实绿色的部分也随着隔离级别发生了变动。以前这两行的记录是X,REC_NOT_GAP,在这里变成了X。
X/S是下一键锁的lockmode,表示一个record-lock R和一个gap-lock (R-1, R)。这个命名实在是太容易引发迷惑了。, 举个例子来讲,被标记X的(555,1)其实表明了两个锁:
在讲table锁以前总结一下讲过的行锁,请参考下图:
select * from tb_user_complaints where user_id = 555 for update;
复制代码
相对行锁,表锁就没有那么经常遇到了。
在row record上加锁时,table schema可能会被直接修改掉,或者被drop掉。为了防止这种table级别的操做,innodb设计了意向锁(intention lock)来帮助快速确认一个table内部是否是有transaction正在进行锁操做或者其余的DML操做。总结一下 意向锁是一种表级锁,lockType是table,lockMode为IX/IS;但意向锁用意是表内有行操做因此要对表的操做作出限制,因此意向锁更多的是一种datalock并不算是真正的metalock。 对行的S锁操做触发的意向锁为IS锁,对行的写行为(update,insert相关)触发的意向锁为IX锁。
因为IX和IS并不会致使table自己被锁定因此IX和IS都是一种共享锁。在metaLock的角度,IX是shared write锁,IS是shared read锁。
当咱们执行select * from tb_user_complaint where user_id = 555 for share;时会发现datalocks和metadata_locks表中会出现意向锁的记录:
表的另外一种锁就是和行锁对应的X锁与S锁,table级别的x锁与s锁和row级别的x锁和s锁至关相似,只是它锁的对象是table相关的元数据,从而这种锁是一种真正的metalock。 当咱们进行DDL时,好比rename,alter或者drop首先要得到metalock的写锁。若是要显示的得到和释放table X/S lock须要执行下面语句。
LOCK TABLES table_name [READ | WRITE]
UNLOCK TABLES;
复制代码
关于table对象上的表也能够经过一个表格来表示互斥共享关系:
这个很好理解,由于X锁每每表明要对lock object作修改,那么lock object自己的其余修改就要排队,object内部数据的锁定也要排队。因此X锁与全部意向锁(表明table内部数据)也都互斥。
S与S类的锁(S,IS)兼容这个很好理解,你们都是锁定读不发生写事件,互不干扰;S锁与X锁互斥也很好理解,这个机制和row的x与s互斥同样。可是笔者以前一直不能理解S锁为何要与IX锁互斥呢,锁定table的meta data不能变更和改动table内部data的IX锁有什么关系呢?其实若是跳出meta这个局限把S锁理解成对table一个总体加的锁就很好理解了,S锁锁住了整个table,table的一切都只能读不能写不管是meta仍是data- -!
因为I锁表明的是行数据的锁状况,因此I锁之间不会发生互斥问题。就算是由于I锁对应的是同一行,互斥问题也由行锁来处理,毕竟I锁是表级所已经再也不负责具体的行信息了。
意向锁的兼容刚刚讲了,IX与XS互斥若是理解了XS就很好理解了,X和S都冻结了整个表的更改固然不能容忍标识行更改的IX存在了。
IS与S不互斥由于都是读互不影响,IS与IXIS不互斥由于都是标识的data,IS与X互斥因为X每每表明要对schema发生变动而schema变动每每会致使一行发生变动与S锁的目的相违背因此不能共存。
下一篇主要讲一下几个问题:4种隔离级别,不一致性的几种状况(脏读幻读和不可重复读),在不一样隔离级别下none-locking reading是如何作到consistent的,在不一样的隔离级别下locking reads又是怎么加锁解决不一致问题的。
但愿你们以为有所帮助的不吝点赞哦,你们的支持是我继续整理案例的动力!