锁的不同角度分类分析

1、锁的不同角度分类

借用尚硅谷课堂的图片在这里插入图片描述

1.1从数据操作的类型划分:读锁和写锁

对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写读-写或者写-读这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决它们。在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写读-写或者写-读 情况中的操作互相阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock或S Lock)排他锁(Exclusive Lock或X Lock),也叫读锁(readlock)写锁(write lock)

  • 读锁:共享锁或S锁。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,互相不阻塞的。
  • 写锁:排他锁或X锁。当前写操作未完成前,他会阻断其他写锁和读锁。这样就能确保在给定的时间内,只有一个事务能写入,并防止其他用户读取正在写入的同一资源。
    PS:对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
    举例(行级读写锁):如果一个事务T1获得了某个行r的读锁,那么此时另一个事务T2是可以去获得这个行r的读锁,因为读取操作并没有改变行r的数据;但是呢如果某个事务T3想获得行r的写锁,则它必须等待事务T1、T2释放掉行r上的读锁才行,因为T3需要修改数据。简单说,行r有事务在读数据,其他读取该数据的事务可以拿到锁,写数据的事务等待。

1.1.1锁定读

在采用加锁方式解决脏读、不可重复读、幻读这些问题时,读取一条记录时需要获取该记录的S锁,其实不严谨,有时候需要在读取记录时就获取记录的X锁,来禁止别的事务读写该记录,为此MySQL给出两种比较特殊的select语句格式:

  • 对读数据的事务加S锁:SELECT ... LOCK IN SHARE MODE;或者SELECT ... FOR SHARE;(8.0的新语法),再有事务想获取该行记录的写锁,只能等待;
  • 对读数据的事务加X锁:SELECT ... FOR UPDATE;
  • MySQL8.0新特性:在5.7之前版本,SELECT … FOR UPDATE如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0版本中,SELECT … FOR UPDATE或SELECT … FOR SHARE添加NOWAIT、SKIP LOCKED语法,意即跳过锁等待或者跳过锁定;这两个参数可以使sql立即返回,如果查询的行r已经加锁,那么NOWAIT会立即报错返回;SKIP LOCKED也立返回,只是返回的结果集中不包含被锁定行的数据;借用尚硅谷课堂的图效果如下:在这里插入图片描述

1.1.2写操作

所说的写操作无非是DELETE、UPDATE、INSERT这三种:

  • DELETE:对一条记录做delete操作的过程其实是先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark操作。我们也可以把这个定位待删除记录在B+树种位置的过程看成是一个获取X锁的锁定读。
  • UPDATE:在对一条记录做UPDATE操作时分为三种情况:
    情况1:未修改该记录的键值,且被更新的列占用的存储空间在修改前后未发生变化。则先在B+树种定位到这条记录的位置,然后再获取一下记录的X锁,最后在原纪录的位置进行修改操作。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读
    情况2:未修改该记录的键值,且至少有一个被更新的列占用的存储空间在修改前后发生变化。则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读,新插入的记录由insert提供的隐式锁进行保护。
    情况3:修改了该记录的键值,则相当于在原纪录上做DELETE操作之后再来一次INSERT操作,加锁操作就按照DELETE和INSERT的规则进行了。
  • INSERT:一般情况下,新插入一条记录的操作并不加锁(因为没有记录,哪里知道锁加在哪),通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。

1.2 从数据操作的粒度划分:表级锁,叶级锁,行锁

为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作)。因此数据库系统需要在高并发响应系统性能两方面进行平衡,这样就产生了锁粒度的概念。对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被称为表级锁或表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗,锁的粒度主要分为表级锁、页级锁和行锁。

1.2.1 表锁

该锁会锁定整张表,是MySQL中最基本的锁策略,并不依赖于存储引擎(不管啥存储引擎,对于表锁的策略是一样的),并且表锁是开销最小的策略(因为粒度比较大).由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题.当然锁的粒度大所带来的的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣

  • 表级别的S锁、X锁:myisam只支持表级锁,innodb可以支持行级锁,因此执行insert、delete、update、select语句时,innodb引擎不会加行级锁。但是在对表执行alert table、drop table这类的DDL语句时,其他事务对这个表的并发操作(增删改查)是阻塞的,同样在对表执行增删改查时,其他事务并发执行DDL这类语句,也会被阻塞。这个过程其实是通过在server层使用一种称之为元数据锁(MDL)结构来实现的。一般情况下,不会使用innodb引擎提供的表级别是S锁和X锁,只会在某些特殊情况下,比如崩溃恢复过程中用到。比如在autocommit=0,innodb_table_locks=1时,手动获取innodb引擎提供的表T的S锁和X锁可以如下方法:LOCK TABLES T READ;(加S锁),LOCK TABLES T WRITE;(加X锁)。PS:尽量避免在innodb引擎下的表加表级锁,并不会提供额外的保护,但是会降低并发能力,因为innodb提供了行锁。
  • 表级别的意向锁
    Innodb支持多粒度锁,它允许行级锁表级锁共存,而意向锁就是其中的一种锁。意向锁是我们在执行某些操作时mysql自动添加的。它的存在是为了协调行锁和表锁的关系,支持多粒度(表锁和行锁)的锁并存;是一种不与行级锁冲突的表级锁(很关键);意向锁存在表明某个事务正在某些行持有了锁或该事务准备去持有锁。
    意向锁有两种:
    • 意向共享锁(IS):事务有意向对表中的某些行加共享锁(S锁)
      --事务要获取行锁,得先获取到IS锁 SELECT ... LOCK IN SHARE MODE;此sql执行完mysql会主动的为我们加上IS共享锁
    • 意向排它锁(IX):事务有意向对表中的某些行加S锁--事务要获取行锁,得先获取到IX锁 SELECT ... FOR UPDATE;此sql执行完mysql会主动的为我们加上IX排它锁
      即:意向锁是有存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加S/X锁之前,innodb会先获取该数据行所在数据表的对应意向锁

意向锁要解决的问题:现在有两个事务T1和T2,其中T2试图在该表级别上应用S锁或X锁,如果没有意向锁存在,那么T2就需要去检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞.T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁.简单说就是给更大一级别的空间(比如行所在的页或者表)里面是否已经上过锁,便于其他事务在操作该表数据时,不需要去每行遍历检查是否有锁。
在数据表的场景中,如果我们给某一行数据加上排它锁,数据库会自动给更大一级空间(比如数据页或数据表)加上意向锁,告诉其他事务这个数据页或数据表已经被上锁了,这样当其他事务想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排它锁即可。其中意向锁、S锁、X锁的兼容性如图:
在这里插入图片描述
针对上图举个例子:事务T1执行select * from table1 where id=2 for update;这时该表会加上IX锁,数据行会加上X锁;此时事务T2执行lock tables table1 read;那么此sql会被阻塞S锁),看图理解下,但是意向锁之间是兼容的,否则就会把意向锁变成表级锁,不符合设计的初衷。
总结如下: 1.Innodb支持多粒度锁,特定场景下,行级锁可以与表级锁共存;2.意向锁之间互不排斥,但除了IS与S兼容外,其他都是互斥;3.IX、IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突;4.意向锁在保证并发性的前提下,实现了行锁和表锁共存满足事务隔离性的要求

  • 表级别的自增锁:AUTO-INC锁,当我们设计表有个字段设置成auto_increment,那么在insert时候会用到这个锁,理解即可,不详细阐述。
  • 表级别的元数据锁
    MySQL5.5引入了meta data lock,简称MDL锁,作用是保证读写的正确性。比如一个事务正在遍历表中的数据,而执行期间另一个事务对这个表结构做变更,增加了一列,那么事务拿到的结果跟表结构对不上,肯定不行.因此,当对一个表做增删改查操作时,加MDL读锁;当要对表做结构变更操作时,加MDL写锁。 这个锁不需要显式使用,在访问一个表的时候会被自动加上。

1.2.2 Innodb中的行锁

行锁即锁住某一行(某条记录).注意:MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现
优点:锁定粒度小,发生锁冲突概率低,可以实现并发度高
缺点:锁的开销都比较大,加锁会比较慢,容易出现死锁情况
innodb与myisam的最大不同点有两:一式支持事务,二是采用了行级锁

  • 记录锁:仅仅把一条记录锁上。分为S型记录锁X型记录锁
    1.当一个事务获取到一条记录的S型记录锁后,其他事务可以继续获取该记录的S型记录锁,但不可以继续获取该记录的X型记录锁
    2.当一个事务获取到一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取该记录X型记录锁
  • 间隙锁:MySQL在repeatable_read隔离级别下是可以解决幻读,解决办法有两个,一是MVCC方案,二是加锁;但是加锁的方案有个问题,即事务在第一次执行读取操作时,那些幻读记录尚不存在,我们无法给这些幻影记录加上记录锁。因此innodb提出了一种称之为gap lock的锁,gap锁的提出仅仅是为了防止插入幻影记录而提出的。比如有一个事务想插入一条ID值为4的新记录,它定位到该条新记录的下一条记录的id值为8(这条记录老早已经写入库中,因为insert时候ID给的值不连续),而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,才能将新的记录插入表中。该锁容易产生死锁.行级锁都容易产生死锁。
  • 临建锁:既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以innodb提出一种被称为next-key locks的锁,简称next-key锁。这个锁在引擎innodb和事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是next-key locks。该锁本质是一个记录锁和一个gap锁的合体,它既能保护该记录,又能阻止别的事务将新记录插入被保护记录前边的**间隙*。举例如下:
    事务A执行 select * from table1 where id <=15 and id >8 for update;此sql表示在(8,15]区间内加上间隙锁,因为id=18也加锁了;事务A未commit时,事务B执行select * from table1 where id =15 lock in share mode;那么事务B就会被阻塞(体现出记录锁),等待事务A的结束才可执行;事务C执行insert into table1(12,‘abc’);那么事务C也会阻塞(因为事务A占着gap锁)。
  • 插入意向锁:我们说一个事务在插入一条记录时需要判断插入位置是不是被别的事务加了gap锁,如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是innodb规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待,innodb就把这样的锁称为插入意向锁(insert intention locks),是一种gap锁,在insert操作时产生。

1.2.3页锁

页锁是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一页有多个记录.页锁的资源开销介于行锁和表锁之间,也会出现死锁,并发度一般。每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的,当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如Innodb中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但是数据的并发度也低了。

1.3从对待锁的态度划分:乐观锁和悲观锁

从对待锁的态度看锁的话,可以将锁分为乐观锁和悲观锁。注意乐观锁和悲观锁并不是锁,而是锁的设计思想

1.3.1 悲观锁

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到拿到锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中的synchronizedreentrantlock等独占锁就是悲观锁思想的实现。
悲观锁其实是借助上面介绍的S锁或者X锁实现的,比如select … for update是mysql的悲观锁。PS: select … for update语句执行过程中所有被扫描的行都会被上锁,因此在mysql中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住,(想象一下B+树,有索引可以直接抵达目的地,没有索引那就一行一行找,这些被找过的行也会被加锁,即使这些行不是我们要找的)。
悲观锁不适用的场景较多,他存在一些不足,因为悲观锁大多情况下依靠数据库的锁机制来实现,以保证程序的并发访问,同样这样对数据库性能开销影响大,特别是长事务而言,这样的开销往往无法承受,这时需要乐观锁。

1.3.2 乐观锁

乐观锁认为对同一数据的并发操作不会总发生,属于小概率时间,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者CAS机制实现。**乐观锁适用于多读的应用类型,这样可以提高吞吐量。**在Java中Java.util.concurrent.atomic包下的原子变量类就是用了乐观锁的一种实现方式:CAS实现的。

  • 乐观锁的版本号机制
    在表中设计一个版本字段version,第一次读的时候,会获取version的值;然后对数据进行更新或者删除操作时,会执行update … set version=version+1 where version = version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。
  • 乐观锁的时间戳机制
    时间戳跟版本号机制一样,也是更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果一致则更新成功,否则就是版本冲突。

1.3.3 两种锁的适用场景

1.乐观锁适合读操作多的场景,相对来说写的操作比较少,优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作
2.悲观锁适合写操作多的场景,因为写的操作具有排他性.采用悲观锁的方式,可以在数据层面阻止其他事务对该数据的操作权限,防止读-写和写-写的冲突。
在这里插入图片描述

1.4 按加锁的方式划分:显式锁和隐式锁

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

1.5 其他锁之全局锁

在这里插入图片描述

1.6 其他锁之死锁

两个事务都持有对方需要的锁,并且在等待对方释放,并且对方都不会释放自己的锁。
在这里插入图片描述

1.6.1 产生死锁的必要条件:

1.两个或者两个以上事务
2.每个事务都已经持有锁且申请新的锁
3.锁资源同时只能被同一个事务持有或者不兼容
4.事务之间因为持有锁和申请锁导致彼此循环等待
死锁的关键在于:两个或两个以上的session加锁的顺序不一致。

1.6.2 如何解决死锁

方式一:等待,直至超时(innodb_lock_wait_timeout=50s)
即当两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚,另外事务继续进行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout用来设置超时时间。
缺点:对于在线服务来说,这个等待时间往往是无法接受的.那么将此值改小(比如1s,0.1s),会容易误伤到普通的锁等待
方式二:使用死锁检测进行死锁处理
innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要并进入等待时,innodb_lock_wait_timeout算法就会被触发。这是一种较为主动的死锁检测机制,要求数据库保存锁的信息链表和事务等待链表两部分信息(借用尚硅谷课堂的图)
在这里插入图片描述
在这里插入图片描述

1.6.3 如何避免死锁

1.设计合理的索引,让业务sql尽可能提高索引定位更少的行,减少锁竞争
2.调整业务逻辑sql执行顺序,避免update/delete长时间持有锁的sql在事务前面
3.避免大事务,尽量将大事务拆分成多个小事务处理,小事务可以缩短资源锁定的时间,也就发生锁冲突的几率更小
4.不要显式加锁,特别是在事务里显式加锁,比如select … for update语句
5.降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调为RC,也可以避免很多因为gap锁造成的死锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值