当前位置: 首页 > 科技观察

重点!你还在为MySQL中的“锁”感到困惑吗?

时间:2023-03-20 18:09:47 科技观察

基本概念01.如何理解“锁”简单来说,锁是数据库中的一种机制,用来处理多个事务之间的协作关系。或者是数据表的标记,用来表示资源的当前状态是否被某些事务占用。这是一个虚构的概念。根据加锁策略可分为记录加锁、间隙加锁和next-key加锁。锁粒度可以分为行级锁和表级锁。InnoDB可以加行锁和表锁;MyISAM只能根据加锁的影响来加表锁,可以区分共享锁(sharelocking,S锁)和排他锁(exclusivelocking,X锁),两者又分别称为读锁和写锁。在事务上锁之前,必须先发出一个“请求”,于是产生了意向锁(intentionlock)。locking),相当于给引擎发送了一个锁意向:又可以细分为共享意向锁(intentionsharelocking,IS)和排他意向锁(intentionexclusivelocking,IX)。occupied)成为对应的S锁或X锁,否则处于等待状态或超时退出。03.增加“加锁”过程加锁过程一般分为两个阶段,即加锁阶段和解锁阶段,所以也叫两阶段加锁。锁的范围是事务。因此,锁定只能在事务开启后由某些SQL语句触发,在事务提交或回滚时释放锁。04.给谁加“锁”并不是所有的SQL语句都加锁,比如DDL(数据定义语言)和DCL(数据控制语言)不涉及事务,自然不存在锁问题,也不是所有的DQL(具体数据querylanguage,suchasselect...)是加锁的,比如普通的select语句是不加锁的,而是依靠MVCC(multi-versionconcurrencycontrol,即多版本并发控制)实现“一定”的一致性的交易。普通的select语句不加锁。如果要加锁,只需要在select语句后面指定“forshare”或“forupdate”,前者是共享锁(S锁),也叫读锁;后者是排他锁(X锁),也叫写锁,但所有DML语句(数据操作语言、插入、更新、删除)都会自动加锁,加上排他锁(X锁)。锁定策略往往需要在一致性(consistency)和并发性(concurrency)之间进行折衷。加锁是为了平衡数据的一致性和并发性。MySQL中实现这种无锁机制的方法是MVCC,著名的多版本并发控制;相应地,通过加锁实现的并发机制称为LBCC(locking-basedconcurrencycontrol)06。加一个“锁”对象表锁就是对整个表加锁。如果是虚拟视图(view),触发trigger,它会锁住与其关联的所有表。实际加锁的对象不是行,而是索引锁,也就是说锁不会定位到某条记录,而是间接通过索引来限制。行事记07.《锁》与事务SQL通用标准定义事务的四个ACID属性,即原子性Atomcity,一致性consistency,隔离isolation,持久性Durability为了实现隔离和保证一致性,需要实现事务;事务的实现依赖于存储引擎。MySQL常用的两个引擎中,默认的引擎是InnoDB,它支持事务,而MyISAM不支持上面提到的,普通的查询语句不加任何锁。这时候innoDB引擎依赖MVCC机制来实现数据库的隔离和一致性。MVCC简单的说就是在可能存在并发和争议的记录中添加带有版本信息的隐藏字段,比如时间戳,保证多次查询数据的一致性。一致状态因隔离级别而异。SQL92标准(数据库的通用标准,不是MySQL独有的)定义了四种Large隔离级别:未提交读(ReadUncommitted,RU),即一个事务可以读取其他事务已经操作但未提交的数据。当这个操作被回滚时,就会发生脏读b.已提交读(ReadCommitted,RC),即一个事务只能读取其他事务提交的数据,保证这个数据是真实数据,避免脏读,但是可能会造成事务前后查询结果不一致窗口,即不能重复读c.RepeatableRead(RR),即可重复读,基于MVCC机制,在当前事务中第一次查询时记录一个快照版本,同一事务中的后续查询使用当前快照版本的结果。因此,即使其他事务提交的数据,如果其快照版本在本事务的第一个快照版本之后,也不会被读出。注意这里当前事务收集的快照“版本号”取决于第一次查询的时机,而不是事务开始的时机。d.可序列化(Serializable,SE),严格限制并发,在多个事务之间存在数据竞争时串行执行。数据稳定性和一致性最强,但并发能力受到很大限制。注意这里指的是数据冲突时事务的序列化,否则并不是所有的数据库都必须包含这4个隔离级别(比如Oracle数据库主要支持RC和SE这2个隔离级别),不同数据库的实现方式并非所有的都是一样的。MySQL支持所有4种隔离级别,默认为RR级别。默认情况下,MySQL执行的每条SQL语句都是自动提交的。如果要显式执行事务,有两种方法:1##启动事务的两种方法2--一种是显式开启事务3STARTTRANSACTION/BEGIN4--另一种是关闭自动提交5SETautocommit=067##结束事务8COMMIT/ROLLBACK对于没有显式开启事务的SQL语句,可以看做是语句前后自动开启并提交事务,即:1select...;2相当于3STARTTRANSACTION;4selece。..;5提交;08。"readphenomenon"阅读现象,官方文档给出的英文措辞,没有找到相关的权威翻译术语。具体指MySQL读过程中的副作用,如脏读、幻读等读现象,主要指数据库中的三种“错误”读结果:Dirtyread:脏读,即A事务读B事务发生变化但未提交的信息,主要发生在RU隔离级别non-repeatableread,不可重复读,即由于B事务在A事务期间发生变化和提交数据,A事务读取前后结果不一致,幻读,Phantomread,即A事务在后面的查询中有一条前面查询没有出现过的记录。鉴于有些资料混淆了幻读和不可重复读,这里说一下幻读和不可重复读的区别:不可重复读,顾名思义,就是前两次的阅读结果而后续的读不一致,这里的不一致涵盖的范围很广,换句话说只要是不一致的,就是不可重复读。主要原因是在一个事务执行过程中,其他事务对数据表做了修改并提交(如果不提交也能读取,性质更差,是脏读),主要发生在RC隔离级别,因为RC的意思是“Readcommitted”,所以任何其他已经向这个事务提交了数据更新的事务都可以知道它。当然前后结果可能不一致,幻读。顾名思义,就是阅读以前没有找到的记录。当然,从某种意义上说,它必须被认为是以前没有发现的不可重复读。这种理解本身是正确的,只是两者的侧重点不同。幻读重点是在本次交易执行过程中,其他交易插入(insert)新的记录,导致本次交易读取到前段时间未发现的交易,就好像发生了幻觉,称为幻读。需要指出的是:MySQL依赖于MVCC的快照机制。RR隔离级别在一定程度上避免了幻读,但还是可以触发。官方文档也给出了相应的说明。详情请阅读下方实战案例。09.SnapshotreadandcurrentreadSnapshotread,snapshotread,也称为一致性读或非锁定读,一致性非锁定读,指的是不依赖锁定来保证查询数据的一致性,是RR和RC级别的默认查询MySQL中的语句执行方式是通过MVCC机制根据“快照”版本号实现读操作。RR级别和RC级别收集“快照”的原理不同,这就是为什么两个隔离级别会出现不同的“读图像”(不可重读或幻读)的原因。其中:RR级别为进入事务后的第一次读操作快照的时间作为快照版本(注意是第一次读操作的时间,与打开事务的时间无关).一旦确定了快照版本,快照结果将被应用到本次事务后续的读操作中。RC级别是每次读操作都会收集快照,所以当有其他事务提交时,可以及时收集新的快照。普通查询语句中,RC级别不属于脏读一致读SE级别,因为依赖锁(默认加普通select语句)。Slock)实现数据一致性,可以保证读出的结果一致,但不再是原来的一致性读当前读,也叫lockedread,即加锁读,指的是加“forshare”或“forupdate”指定读操作是sharedread还是exclusiveread,其中:forshare,即加S锁,允许多个事务同时获取S锁,称为sharedforupdate,即加X锁,只有获取X锁的事务操作是排他的。由于锁读是基于事务的,所以必须显式开启,锁读才有意义。否则,交易实战案例中以下所有案例均依赖于NavicatPrimium12工具。初始建表语句:1createtabletest(idint,namevarchar(20),primarykey(id));2insertintotestvalues(1,'A');3insertintotestvalues(3,'C');10.3种“readlike”脏读,不可能重复读和幻读应该是困扰很多人的常见概念问题,尤其是后两者的区别。下面举几个案例来说明。Dirtyread,dirtyread先看官方文档给出的定义:一种取回不可靠数据的操作,即被另一个事务更新但尚未提交的数据。只有使用称为未提交读的隔离级别才有可能。在该操作中,处理其他事务更新但尚未提交的数据。此数据为不可靠数据,仅出现在RU隔离级别。案例:RU有脏读:事务A读取了事务B的变化但未提交的数据不能重复读取,不可重复读官方文档给出的定义:一个查询获取到数据,后面的查询在同一个事务内的情况检索应该是相同数据的内容,但查询返回不同的结果(同时被另一个事务提交更改)。大致思路:一个事务在查询数据的过程中,由于其他事务同时提交,导致前后两次查询的数据结果不一致。案例:RC避免了脏读,但是存在不可重复的幻读,幻读出现在一个查询的结果集中,但不在更早的查询的结果集中的一行。例如,如果一个查询在一个事务中运行两次,同时,另一个事务在插入新行或更新行之后提交,以便它与查询的WHERE子句匹配。大意:在上一次查询的结果中不存在,而在查询后得到的记录,称为幻读。例如,一个查询被执行了两次,期间另一个事务插入或更新记录并提交,导致前一个事务的两次查询结果不一致。从我个人的角度来看,幻读本身当然是一种不可重复的阅读。毕竟,两次读数的结果“不一致”。但是幻读着重于之前不存在的具体操作,然后幻读一个新行。案例:①、RR级别可以避免RC级别的不可重复读问题:RR没有不可重复读数据②、特殊情况下仍然可以触发幻读RR级别下,特殊操作仍然可以触发幻读reads(updatesnapshots)actually一般情况下,MVCC机制只做快照来保证读到的结果被读到,所以可以保证可重复读,但是在执行insert、update、delete操作的时候,实际上还是会检测最新的记录当前数据库中的状态:当其他事务commit最新数据满足本事务中增删改操作的条件时,仍会受到影响。这不难理解。毕竟数据库的状态一致性是要保证的,但是更新之后,事务中的快照版本会被更新,这让人很意外。比如图中的情况,初始查询时有2条记录,更新时实际更新了3条记录,但第二次查询时结果也更新为3条记录。而且,更重要的是,这种现象并不普遍:只有在事务执行更新操作时才会更新快照版本,而对于删除和插入操作,只是检测状态,不会更新快照版本。事务的插入操作不会更新快照版本。更一般地,进一步测试事务B进行的其他增删改对事务A是否更新快照版本的影响。结合两对,得到以下实验结论:以上幻读只发生在其他事务插入新记录并提交后,在事务更新数据后再次查询,当然官方文档给出了注解对此:大意是:快照读取(snapshot)只适用于查询语句,而DML(数据操作语言,即增、删、改操作)不适用。其他事务执行删除或更新操作并提交,而当前事务“看不到”这些更改,但在执行了自己的更新或删除操作后变得可见。虽然这个笔记足以说明上述案例的结论,但笔者其实对上表还是有疑惑的。最后需要指出的是,MVCC机制是基于快照版本的并发控制,对应的是LBCC。使用LBCC读取数据时,总是能读取到最新的数据。当然,这与RR隔离级别和MVCC机制并不矛盾。Lockedread始终读取最新的结果,但不影响快照版本11。快照版本MVCC基于多版本并发控制,查询结果基于快照版本。但是不同隔离级别的快照版本收集原则是不一致的。在RR隔离级别,通过MVCC机制实现了同一事务中的可重复读问题,快照是第一次查询时收集的版本号信息,与打开事务的时机无关。快照版本是为RR层级的第一次查询建立的,一旦RR层级建立了快照版本,该事务的后续查询就会以快照版本作为结果(当然也有通过查询发现的例外情况)以前的案例);对应的是,在RC层面,每次查询都会收集最新的快照版本作为结果,自然就存在不可重复读的问题。12、加锁的种类首先简单介绍一下记录锁、间隙锁和相邻键锁:记录锁记录锁根据索引锁定对应的记录,即使对应的表没有建立索引。事实上,所有的InnoDB表都有索引。当用户在创建表时没有显式设置索引时,引擎会自动创建一个隐藏索引。这也是由于InnoDB底层基于聚集索引来访问整条记录的特性。记录锁只锁定索引满足查询条件的记录。间隙锁。如果记录锁锁定命中记录,那么间隙锁保留并锁定查询范围内不存在的记录。比如下图中,假设表中不存在id=2和3的记录,但是因为符合查询范围,所以会在上面加间隙锁。Gaplock锁定满足查询条件的记录的间隙。显然,间隙锁牺牲了一定的并发性能来换取高一致性。其实所有的锁都是这样做的,就是在一致性和并发性之间取得一定的平衡。需要指出的是,间隙锁只存在于范围查询中,并不适用于等值查询。比如上面例子中查询条件改成whereid=1或者id=4,那么潜在的id=2和3就不会添加了。间隙锁当查询条件为等价查询,但查询条件为联合索引(在多列上创建的索引)时,也会对符合要求的潜在记录加间隙锁。间隙锁只存在于特定的隔离级别,在RR级别默认有间隙锁,但在RC级别没有键锁。在记录锁和间隙锁的基础上,键锁=记录锁+间隙锁。相邻键锁=记录锁+间隙锁RC隔离级别只有记录锁,没有间隙锁和相邻键锁;在RR级别,如果是等值查询就是记录锁,范围查询就是相邻键锁(即记录锁+间隙锁),在5.6之前的版本可以设置是否开启它通过全局参数,但这个变量在8.0版本中已被删除。RC隔离级别默认为记录锁。RR隔离级别默认为键锁。13、索引类型对锁的影响指定锁类型后,需要考虑不同索引对锁的影响。首先要指出的是,即使在InnoDB引擎下建表时没有明确指定索引,引擎也会自动生成一个隐藏索引用于聚类和存储记录数据。基于此,索引对锁的影响有以下几种情况(引用自官方文档):一致性读(即快照读,非锁读,基于MVCC),除了SE隔离级别,其他隔离级别不加任何Lockthecurrentread(lockedread,forshareorforupdate),锁定所有满足条件的记录,同时释放不满足条件的记录。对于一些复杂的语句,比如Union语句,由于汇总结果时涉及到临时表,对于不满足查询条件的记录,不会立即释放锁。同时,加记录锁还是加键锁取决于索引类型和查询条件。只有使用对应唯一索引下的等价查询时,才加记录锁,否则升级为键锁。update语句会在满足每个record语句的情况下加key锁(X锁),但只有在满足唯一索引和等价查询时才加record锁。delete语句的加锁原理和update语句一样。没有间隙锁。其实insert语句就是先加一个意向锁,等请求成功后再插入,否则不会阻塞其他事务。特殊情况下,当多个事务同时插入同一条索引记录时,会出现索引重复冲突,可能会造成死锁。有关详细信息,请参阅下一节。关于不同类型下加锁的详细分析,请参考文末参考文献2中的文档。解释充分,被广泛转发和引用。14、锁竞争与死锁一般来说,锁是排他的。如果是共享锁(S锁),则可以与另一个共享锁(S锁)同时拥有,但不能与独占锁(X锁)同时拥有;对于X锁,它不能与任何其他锁并发。当多个事务试图同时占用一个资源并需要加锁时,可能会发生锁竞争甚至死锁。锁竞争,当多个事务试图同时占用同一个资源,但只是时间上冲突而资源占用上没有冲突时,就会出现锁竞争:多个事务争夺同一个资源如上例,三个事务依次请求data给表加X锁,这里事务A请求成功,事务B和事务C会等待。事务A提交事务后,虽然事务B和事务C同时处于争锁状态,但是由于MySQL事务调度的FIFO(先进先出)特性,两者不会死锁,但是先满足事务B的锁请求,事务B提交事务后满足事务C的锁请求。死锁,与锁竞争类似,但不同的是,当多个事务同时竞争同一个资源时也会出现死锁,但这些资源并不能简单地通过时间来解决,而是存在逻辑冲突:①、锁竞争+索引重复冲突导致死锁:三个事务争夺资源,索引重复。这种情况与锁竞争中的例子类似但又有所不同:假设事务A、事务B、事务C同时请求插入一条数据(插入语句为X锁),此时不仅因为锁冲突,还有索引重复的问题。此时一旦事务A回滚释放锁,事务B和事务C就会陷入死锁。这是一个特殊的死锁触发器。②、竞争同一个资源出现死循环:两个事务先竞争,然后死锁。这种情况下,首先事务A和事务B分别给id=1和id=2的记录加X锁,然后事务A继续给id=2的记录加锁,当id=2的记录请求加锁时,因为记录已经被事务B占用,事务A只能等待;但此时事务B试图对事务A已经占用的id=1的记录加X锁,导致事务A和事务B在各自拥有某某的基础上试图占用对方的锁定资源大量的资源,这在逻辑上是矛盾的,很难下车。引擎无法通过时间调度来解决,所以会出现死锁。发生死锁后,引擎会根据相关事务的重要性(包括占用了多少资源,时间顺序等)选择一个进行回滚:比如上面的例子中,事务A先于事务B请求X锁,而事务B被认为是死锁的直接原因,所以选择回滚B,让A加锁成功。如果你能看到这里,相信你应该对MySQL中的锁机制有了更全面的了解,欢迎转发或观看!