目录
1. 从粒度上区分锁
1.1 全局锁(第一粒度)
1.2 表级锁(第二粒度)
1.3 行锁(第三最小粒度)
2 从模式上区分锁
2.1 什么是乐观锁
2.2 什么是悲观锁
2.3 意向共享锁和意向排他锁
2.4 临键锁和记录锁
2.5 间隙锁
MySQL中的锁定主要分为全局锁、表锁和行锁;
MySQL全局锁是针对整个数据库的锁;
1. 从粒度上区分锁
1.1 全局锁(第一粒度)
全局锁:
- 读锁(共享锁,S锁):它阻止其他用户更新数据,但允许他们读取数据;这在你需要在一段时间内保持数据一致性时很有用;
- 写锁(排他锁,X锁):它阻止其它用户读取和更新数据;这在你需要修改一些大量的数据,并且不希望其它用户在这段时间内干扰时很有用;
MySQL全局锁的典型使用场景是,进行一些需要确保整个数据库一致性的操作,例如全库备份,全库导出等;
在MySQL中,可以使用
FLUSH TABLES WITH READ LOCK(FTWAL)
语句来添加全局锁,这将阻止其它线程进行更新操作;使用UNLOCK TABLES
语句来释放锁定;
注意点:全局锁的开销非常大,因为它会阻止其它所有的数据修改操作,并且在高并发情况下可能导致大量的线程等待锁定;因此,应该尽量避免在生成环境中使用全局锁,或者尽量减少全局锁的持有时间;
输入全局锁有其应用场景,但是过度使用或不正确使用全局锁可能导致性能问题;因此,根据应用的特性和需求选择适合的锁策略很重要;对于大多数应用,优先使用更精细粒度的锁,如行锁和表锁,可以更有效地处理并发请求,同时避免全局锁的开销;
全局锁目的:比如我在备份数据的时候,我在订单中修改了数据,原本只有100个订单,在我备份途中,增到了300个订单,这时候全局锁的作用来了,可以让其它事务的锁等待我锁执行完毕后,才进行添加,直接阻塞其它的锁,保证数据的一致性;
1.2 表级锁(第二粒度)
什么是表级锁?表级锁是MySQL中最基本锁策略,是MySQL最早采用的锁策略;表级锁的特点是开销小,加锁快、不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
表锁的两种模式:
- 表共享读锁(Table Read Lock):又称为表读锁,允许一个事务锁定的表进行读取操作,不允许其它事务对其写操作,但是可以进行读操作;读锁之间是不会互相阻塞的;
- 表独占写锁(Table Write Lock):又称为表写锁,允许一个事务锁定的表读取和写入(更新)操作,但是其它任务事务都不能对改表进行任务操作,必须等待表写锁结束;写锁会阻塞其它所有锁,包括读锁和写锁;
在MySQL中,对MyISAM表的读操作,会自动加上读锁,对MyISAM表的写操作,会自动加上写锁;
InnoDB引擎在必要情况下会使用表锁,但主要是使用行锁来实现多版本并发控制(MVCC),它能提供更好的并发性能和更少的锁冲突;
什么是MySQL的MVCC机制? - 知乎 (zhihu.com)
小结:表锁适用于读操作多、写操作少的应用,当并发争用不是特别激烈,以及记录级锁并发控制开销大于访问冲突开销的情况;在并发度高,或者写操作较多的情况下,表锁可能会称为瓶颈;
表级锁的使用场景:
- 读密集型应用:如果你的应用主要进行读取操作,很少进行写入操作,那么使用表级锁可能是一个好选择;因为表级锁不会阻塞其它的读锁,所以这种场景下表级锁能够提供很高的性能;
- 写操作不频繁的场景:表级锁对写操作的处理并不高效,因为一个写锁会阻塞所有其它的锁,无论它们是读锁还是写锁;但是,如果你的应用不需要频繁地进行写操作,或者可以容忍写操作的延迟,那么使用表级锁可能是可行的;
- 数据量不大的简单应用:如果数据库的数据量不大,那么即使在写操作中,由于锁定整张表,对性能的影响也不大;
- 全表更新或者删除:在某些情况下,可能需要对一张表进行全表的更新或者删除操作,列如,删除表中的所有记录,或者更新表中所有记录的某个字段的值;这种情况下,使用表级锁是合适的;
注意点:虽然表级的开销较小,但由于其锁定粒度大,可能会导致并发度下降,特别是在写操作较多或者并发度较高的场景下;所以应用的并发度较高,或者需要频繁进行写操作,那么可能需要考虑使用更精细粒度的锁,如行锁;
MySQL哪些命令会发生表级锁:
- ALTER TABLE:这个命令用于更改表的结构,如添加列、删除列、改变列的类型等;执行这个命令的时候,MySQL需要锁定整个表以防止在更改过程中有新的数据写入;
- DROP TABLE和TRUNCATE TABLE:这两个命令都会导致表级锁;DROP TABLE会删除整个表,而TRUNCATE TABLE命令会删除表中的所有数据;在执行这些命令的时候,MySQL需要锁定整个表以防止在删除过程中有新的数据写入;
- LOCK TABLES:这个命令可以显示地为一个或多个表加上读锁或写锁;LOCK TABLES命令后面可以跟上一系列的表名和锁模式,用来指定需要锁定哪些锁,以及使用什么样的锁模式;列如LOCK TABLES t1 WRITE,t2 READ;命令会给表t1加上写锁,给表2加上读锁;
- 全表扫描或大范围扫描:对于MyISAM存储引擎,全表扫描或大范围扫描会触发表级锁;
- FLUSH YABLES WITH READ LOCK (FTWRL):这个命令可以给所有表加上全局读锁,其它会话在此期间不能对数据进行修改;
注意点:InnoDB存储引擎主要使用行级锁,并在一些情况下使用表级锁,比如在执行某些ALTER TABLE命令或者LOCK TABLES命令时;MyISAM存储引擎只支持表级锁;
MySQL表锁风险点:
- 性能下降:因为表锁会锁定整个表,所以在高并发环境中,可能会导致大量的请求阻塞,从而降低性能;对于读取和写入混合密集的负载,表锁可能会成为一个性能瓶颈;
- 并发性能差:表锁的最大问题在于其并发性能;一旦一个线程对表获得了写锁,其它线程的任何读写操作都会被阻塞,直到写锁被释放;同样的,如果一个读锁被持有,那么其它的写操作将被阻塞;使得并发性能大大降低;
- 可能导致锁等待和超时:在高并发的环境中,由于表级锁的粒度较大,可能会有很多线程在等待锁,如果等待的时间过长,可能会导致锁超时,进一步影响应用的性能和可用性;
- 写操作影响大:如果一个长时间允许的写操作(比如大数据量的UPDATE或者INSERT语句)获取了写锁,那么会阻塞所有其它的读操作和写操作,直到整个写操作完成;
- 死锁的可能性:虽然表锁本身不会死锁,但是多表操作中,如果没有按照一定的顺序获得锁,可能会导致死锁;
为了避免这些问题,我们通常会选择InnoDB存储引擎,它主要使用行级锁,可以提供更好的并发性能,并且在一定程度上减少了锁争用的问题;而且,InnoDB支持事务,可以保证数据的一致性和完整性;在实际应用中,我们应该根据具体的业务需求和系统负载,选择合适的存储引擎和锁策略;
1.3 行锁(第三最小粒度)
什么是行锁?行级锁是MySQL中一种锁定机制,它可以对数据库表中的单独一行进行锁定;相比于表级锁和页锁,行级锁的粒度更小,因此在处理高并发事务时,能提供更好的并发性能和更少的锁冲突;然而,行级锁也需要更多的内存和CPU资源,因为需要对每一行都进行管理;
在MySQL中,行级锁主要由InnoDB存储引擎提供;InnoDB支持两种类型的行级锁:共享锁(S锁)和排他锁(X锁);
- 共享锁(S锁):共享锁也称为读锁,它允许一个事务读取一行数据;当一行数据被共享锁定时,其它事务可以读取这行数据,但不能对其进行修改;
- 排他锁(X锁):排他锁也称为写锁,它允许一个事务读取和修改一行数据;当一行数据被排他锁锁定时,其它事务不能读取也不能修改这行数据;
在实际使用中,InnoDB还提供了一种名为“间隙锁”(Gap Lock)的特征;间隙锁不仅锁定一个具体的行,还锁定它前后的“间隙”,即这一行之前和之后的行之间的空间;间隙锁可以防止其它事务插入新的行到已锁定的前后,从而可以解决一些并发问题;
值得注意的是,行级锁只在事务中有效,也就是说,只有在一个事务开始(BEGIN)后并在事务提交(COMMIT)或回滚(ROLLBACK)之前,才能对数据行进行锁定;如果在非事务环境中执行SQL语句,那么InnoDB会在语句执行结束后立即释放所有的锁;
MySQL行锁的使用场景:
MySQL中的行级锁(Row Level Locks)通常在以下几种场景中被使用:
- 高并发读写操作:在需要高并发读写操作的场景中,行级锁可以提高性能和并发性,因为它允许多个事务并发地操作不同的行;
- 单行操作:对于需要操作单行数据的SQL语句(例如基本主键或者唯一索引的UPDATE、DELETE和INSERT语句),行级锁可以提供较好的并发性和性能;
- 短期锁:在需要对数据进行短时间锁定的情况下,行级锁可以防止长时间阻塞或其它事务;
- 实现并发控制:在需要确保数据一致性和隔离性的事务中,行级锁是实现并发控制的重要机制;(在MySQL中MyISAM是通过行锁来实现事务的并发控制的,而InnoDB是通过上面的排他锁和共享锁来实现的)
- 复杂的事务处理:在需要对多行数据进行复杂处理的事务中,可以使用行级锁来锁定这些行,防止在事务处理过程中数据被其它事务修改;
使用行级锁需要注意点:由于行级锁的锁定粒度较小,它可能会耗费更多的系统资源(例如内存和CPU),特别是在处理大量数据时;此外,使用行级锁也可能导致死锁,需要使用合适的策略来避免死锁,例如在事务总按照一定的顺序锁定行;
MySQL哪些命令会导致发生行锁?
在MySQL中,主要是InnosDB存储引擎提供了行级锁(Row Level Locking);一般来说,以下这些类型的操作会导致InnoDB对数据进行加锁:
- SELECT ... FOR UPDATE:这种查询会对选定的行添加一个排他锁(X锁),这意味着其它事务不能修改这些行,也不能对这些行添加共享锁;
- SELECT ... LOCK IN SHARE MODE:这种查询会对选定的行添加一个共享锁(S锁),这意味着其它事务不能修改这些行,但可以对这些行添加共享锁;
- INSERT:插入操作会对新添加的行添加一个排他锁(X锁);
- UPDATE:更新操作会对被更新的行添加一个排他锁(X锁);
- DELETE:删除操作会对被删除的行添加一个排他锁(X锁);
这些加锁的操作都是在事务中进行的,即只有在事务开始(BEGIN)后并在事务提交(COMMIT)或回滚(ROLLBAK)之前,才会对数据行进行加锁;如果在非事务环境中执行上述SQL语句,那么InnoDB会在语句执行结束后立即释放所有的锁;
注意点:加锁的粒度和范围取决于WHERE子句中用到的索引;如果WHERE子句中用到了唯一索引(例如主键索引),那么InnoDB只会锁定匹配的行;如果没有用到唯一索引,那么InnoDB可能会锁定更多的行,甚至是整个表,这就可能导致锁冲突和性能问题;
此外,InnoDB还支持间隙锁(Gap Locks)和临建锁(Next-Key Locks),这两种锁都可以在某些情况下提供跟个好的并发控制;
MySQL行锁有什么风险点?
尽管行级锁(Row-Level Locking)可以提供高并发性并减少锁冲突,但在使用过程中也可能遇到一些风险和问题,主要包括以下几点:
- 死锁:当两个或更多的事务相互等待对方释放资源时,就会发生死锁;例如,事务1锁定了行A并试图锁定行B,同时事务2锁定了行B并试图锁定行A,这就形成了死锁;MySQL会检测到死锁并终止其中一个事务,但这仍可能导致性能问题和事务失败;
- 锁升级:如果一个事务试图锁定的行过多,InnoDB可能会将锁从行级升级为表级,这就可能导致更多的锁冲突;
- 锁等待:如果一个事务已经锁定了某行,其他试图访问这行的事务就必须等待,这可能导致性能下降;如果有大量的事务在等待所,就可能导致系统出现性能瓶颈;
- 资源消耗:行级锁需要更多的内存来存储锁信息,而且需要更多的CPU时间来处理锁请求和释放锁;如果数据库中的行数非常多,或者并发事务的数量非常多,这可能会导致显著的资源消耗;
- 难以调试和排查:由于行级锁的粒度较小,如果出现性能问题或锁冲突,可能需要复杂的调试和排查工作来找出问题的原因;
- 事务的隔离级别:不同的事务隔离级别会影响锁的行为和性能,可能需要根据具体的应用场景来调整事务隔离界别;
为了避免上述问题,需要合理地设计数据库表和索引,合理地编写SQL语句,合理地管理事务,以及合理地设置事务隔离级别;
2 从模式上区分锁
乐观锁和悲观锁;这些都是思想;
乐观锁是在表中设置版本号(或时间戳)来进行乐观锁;
悲观锁是根据SELECT ... FOR UPDATE上排他锁,用SELECT ... LOCK IN SHARE MODE上共享锁;
2.1 什么是乐观锁
乐观锁(Optimistic Locking)是一种在数据库操作中用于处理并发问题的技术;它的基本思想是假设在多个事务同时访问同一条数据是,冲突发生的概率较低,因此在操作数据时不会立即进行锁定,而是在提交数据更改时检查是否有其他事务修改了这条数据;如果没有,就提交更改,否则就回滚事务;
在MySQL中,乐观锁并没有内置的实现,但是可以通过一些编程技巧来实现;一种常见的实现方式是使用版本号(或时间戳)字段;每当一条记录被修改时,就增加版本号(或更新时间戳);在更新记录时,先检查版本号(或时间戳)是否和读取记录时的版本号(时间戳)一致,如果一致则执行更新并添加版本号(或时间戳),否则就拒绝更新;这样就可以保证只有当记录没有被其他事务修改时,当前事务的更改才会提交;
乐观锁的优点在于,由于大部分时间都不要锁定,所以冲突较少的情况下可以获得较高的并发性;然而,如果冲突较多,那么乐观锁可能会导致大量的事务回滚,从而影响性能;因此,选择使用乐观锁还是其他锁定技术,需要根据实际地并发情况和性能需求来决定;
乐观锁的使用场景:
乐观锁是一种对并发控制的策略,适用于以下应用场景:
- 低冲突环境:在多数情况下,数据并发修改的冲突较低,即同一时间内,同一条数据不会被多个事务同时修改;
- 读多写少的场景:在读操作远多于写操作的情况下,乐观锁可以避免由于频繁的读操作导致的不必要的锁定开销;
- 短事务操作:如果数据库的事务都是简短并且快速完成的,那么使用乐观锁可以减少因为等待锁而导致的时间消耗;
- 分布式系统:在分布式系统中,由于网络延迟等原因,事务冲突的可能性较低,因此乐观锁是一个合适的选择;
- 互联网应用:对于互联网应用,如电子商务网站,用户浏览商品和下单等操作,多数情况下是读取操作,且并发修改同一条数据的几率较小,因此使用乐观锁可以提高系统性能;
注意点:如果事务冲突可能性较高,或者需要长时间锁定某个资源,那么使用乐观锁可能会导致大量的事务冲突和回滚,这种情况下,悲观锁或者其他并发控制技术可能会是更好的选择;
MySQL中如何使用乐观锁:
在MySQL中,乐观锁并没有明确的SQL语句或命令,通常是通过在应用程序代码中实现特定的逻辑来达到乐观锁的效果;一个常见的实现乐观锁的方式是使用版本号或者时间戳字段;
假设我们有一个名为test_lock的表,其中有一个version字段用于实现乐观锁:
将version作为校验乐观锁的条件,如果改数据被其他事务修改就回滚,没有被修改的话就提交;
乐观锁的缺点:
- 冲突检测:在高并发的环境中,乐观锁可能会导致大量的冲突;因为乐观锁只有在提交事务时才检查是否有冲突,如果多个事务在同一时间操作同一行,那么只有一个事务能提交成功,其他的事务都需要回滚并重新尝试;
- 处理开销:在冲突发生时,需要进行回滚和重试,这可能会增加系统的开销;在一些场景中,这可能会导致性能下降;
- 版本管理:乐观锁通常通过版本号(或时间戳)来检测冲突;这就要求系统能够正确地管理这些版本号,否则可能会导致错误的冲突检测;
- 编程复杂性:使用乐观锁需要更复杂的编程,因为程序需要处理可能发生的冲突和重试;
小结:乐观锁是一种有效的并发控制策略,但在冲突较多的情况下,可能会带来更大的开销和编程复杂性;因此,是否选择使用乐观锁,需要根据应用的具体需求和场景来决定;
2.2 什么是悲观锁
悲观锁(Pessimistic Locking)是一种并发控制的方法,基于一个假设:认为数据在并发处理过程中很可能会出现冲突;因此,为了保证数据的完整性和一致性,每次在读写数据时都会先加锁,这样可以避免其他事务进行并发的读写操作;
是否使用悲观锁需要根据应用的具体需求和场景来决定;在冲突较少,但需要保证数据完整性和一致性的情况下,可以考虑使用悲观锁;
MySQL悲观锁适用哪些场景?
悲观锁的策略是假设数据在并发处理过程中会发生冲突,因此在进行如何读写操作前,都会预先加锁;这种策略在某些特定的应用场景下是比较有优势的,主要包括:
- 写操作较多的场景:如果一个系统中的写操作比读操作多,或者说写操作占主导,那么悲观锁可能是一个比较好的选择;因为在这种场景下,数据冲突的可能性相对较高,预先加锁可以确保数据的完整性和一致性;
- 并发冲突高的场景:在并发冲突较高的场景,使用悲观锁可以避免重复尝试操作,提高系统的整体效率;
- 业务需要强一致性的场景:在一些需要保证数据强一致性的业务场景下,例如银行转账等金融业务,通常会选择使用悲观锁,以确保在任何情况下的数据的一致性和准确性;
但是值得注意的是,悲观锁也可能引入死锁问题,也可能因为锁定过程中事务长时间等待而影响性能;因此,选择和使用悲观锁都需要根据具体业务场景和需求来进行;
MySQL中如何使用悲观锁?
在MySQL中,悲观锁主要通过以下两种SQL语句实现:
- SELECT ... FOR UPDATE:这个语句会在所选的行上设置排他锁(Exclusive Lock);在锁定期间,其他事务无法修改这些行,也无法在这些行上设置新的排他锁或共享锁;
- SELECT ... LOCK IN SHARE MODE:这个语句会在所选的行上设置共享锁(Shared Lock);在锁定期间,其他事务可以读取这些行,但不能修改这些行,也不能在这些行上设置排他锁;
使用悲观锁的例子:
于上述的排他锁(X锁)和共享锁(S锁)一致;
参考:MySQL事务与并发控制案例
悲观锁的缺点:
- 性能开销:在悲观锁机制下,锁定资源的操作会影响到系统性能;因为每次对数据的读写都需要进行加锁和解锁的操作,这会增加系统的开销,特别是在高并发的环境下,锁的竞争更是会严重影响到系统性能;
- 并发度降低:由于悲观锁在操作数据前就会加锁,这就导致了在同一时间,只有一个事务能操作数据,其他事务只能等待,大大降低了系统的并发度;
- 死锁:悲观锁在并发事务中可能导致死锁的情况发生;当两个或者更多的事务互相等待对象释放锁是,就可能发生死锁;虽然数据库系统通常能够检测并解决死锁,但这会导致事务回滚,增加了系统的开销;
- 锁超时:如果一个事务长时间持有锁而不释放,可能导致其他等待锁的事务超时;这不仅可能导致等待的事务失败,还可能影响到整个系统的稳定性;
因此,虽然悲观锁能有效地防止数据冲突,但由于其在并发处理中的限制,以及可能引发的问题,如死锁、锁竞争和锁超时,我们需要根据具体的应用场景和需求,来权衡是否使用悲观锁;
2.3 意向共享锁和意向排他锁
其实就是排他锁和共享锁,但是在InnoDB下,支持行锁,所以存在意向共享锁和意向排他锁;
意向锁是表锁,为了协调行锁和表锁的关系,支持多粒度(表锁于行锁)的锁并存;
当事务A有行锁是,MySQL会自动为该表添加意向锁,事务B如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能;
为什么意向锁是表级锁呢?
当我们需要加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁)
如果意向锁是行锁,则需要遍历每一行数据去确认;
如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能;
*意向锁怎么支持表锁和行锁并存?
- 首先明确并存的概念是指数据库同时支持表、行锁,而不是任何情况都支持一个表中同时有一个事务A持有行锁、又有一个事务B持有表锁,因为表一旦被上了一个表级的写锁,肯定不能再上一个行级的锁;
- 如果事务A对某一行上锁,其他事务就不可能修改这一行;这与“事务B锁住整个表就能修改表中的任意一行”形成了冲突;所以,没有意向锁的时候,让行锁与表锁共存,就会带来很多问题;于是有了意向锁的出现,如前面所言,数据库不需要在检查每一行数据是否有锁,而是直接判断一次意向锁是否存在即可,能提升很多性能;
意向锁的兼容互斥性:
意向共享锁(IS) | 意向排他锁(IX) | |
共享锁(S) | 兼容 | 互斥 |
排他锁(X) | 互斥 | 互斥 |
2.4 临键锁和记录锁
临键锁可以理解为一种特殊的间隙锁,它们的区别可能在于,临键锁的锁定是非唯一索引字段的间隙,而间隙锁的锁定是唯一索引的间隙;
什么是临键锁?Next-Key可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法;通过临键锁可以解决幻读的问题;每个数据行上的非唯一索引列都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据;需要强调的是,InnoDB中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁;
id | age | name |
1 | 10 | 张三 |
3 | 24 | 李四 |
5 | 32 | 王五 |
7 | 45 | 赵六 |
该表中age列潜在的临键锁有:
(-∞,10],
(10,24],
(24,32],
(32,45],
(45,+∞],
例子:
1.我开启了一个锁,指定的是age等于24的这一行;
START TRANSACTION;
SELECT * FROM users WHERE age = 24 FOR UPDATE;
COMMIT;
2.但是执行插入数据age为26时,操作语句被锁住了,说明在锁住age=24行时,age列都存在一个临键锁,导致插入时在等待锁释放;(26存在(24,32]这个区间)
INSERT INTO users VALUES(100, 26, '北冥');
**什么是记录锁?**记录锁(Record Lock)是MySQL数据库中InnoDB存储引擎的一种锁定机制,主要用于锁定和控制对单个行记录的访问;记录锁是在索引记录上设置的,对于表没有主键或唯一索引的表,InnoDB会生成一个隐藏的聚簇索引,并在这个隐藏索引上加锁;
实际操作中,记录锁通常会在进行数据查询、更新或删除等操作对自动被数据库引擎应用;当执行下述查询时,MySQL会在Orders表的order_id为1行上设置记录锁;
SELECT * FROM Orders WHERE order_id = 1 FOR UPDATE;
记录锁主要有两种类型:共享锁(Shared Locks)和排他锁(Exclusive Locks);共享锁(S锁)允许多个事务同时读取同一数据,但阻止任何事务进行写操作(包括该事务本身);排他锁(X锁),又称写锁,当一个事务持有排他锁时,其他事务不能读取也不能写入被锁定的数据;
2.5 间隙锁
什么是间隙锁?间隙锁(Gap Locks)是MySQL InnoDB存储引擎提供的一种锁定机制;它锁定的不是具体的行记录,而是两个索引之间的间隙(或叫区间),这样可以防止新的记录插入到该间隙,确保数据的一致性和事务的隔离性;
间隙锁常常与记录锁(Record Locks)一起使用,共同形成Next-Key锁,保护索引记录的范围查询和扫描操作;
间隙锁的主要类型:
- 区间-区间间隙锁:锁定两个索引键之间的间隙,或者是第一个索引之间的间隙;
- 区间-记录间隙锁:锁定一个索引键和一个记录之间的间隙;
- 记录-区间间隙锁:锁定一个记录和一个索引键之间的间隙;
间隙锁的存在,主要是为了解决幻读问题;所谓幻读,就是一个事务内读取某个范围的记录时,另一个事务在该范围内插入了新的记录,当一个事务再次读取该范围的记录时,会发现有些原本不存在的记录,这就是幻读;
间隙锁避免幻读的方式:
假设我们又一个存储学生信息的表,有一个事务A要查询年龄在10-29之间的学生,它在查询前会对这个区间加锁;此时如果有另一个事务B想要插入一个年龄为15的学生,由于这个年龄的范围已经被事务A锁定,所以事务B必须等待,直到事务A完成,释放锁;这样就避免了幻读的产生;
注意的是:由于间隙锁会锁定范围,如果并发事务较多且涉及的数据范围有交集,可能会引发性能问题,甚至死锁;因此,在涉及数据库和选择隔离级别时,需要综合考虑数据一致性和并发性能;
间隙锁的使用场景:
间隙锁(Gap Locks)在MySQL数据库的InnoDB存储引擎中主要用于以下场景:
- 防止幻读:间隙锁的主要目的是防止其他事务在已经锁定的范围内插入新的行;这可以避免“幻读”问题,即在一个事务读取某个范围内的所有行时,另一个事务插入了一个新行,当第一个事务再次读取该范围时,会发现有一个“幻影”行;
- 范围查询:在执行范围查爱心时,如果事务需要对查询结果间隙更新或删除,那么间隙锁可以保证在事务执行期间,不会有新的行插入到查询范围中;
例如,以下事务在Orders表的order_id列值在1到100之间的所有行上设置排他锁,并在这些的间隙上设置间隙锁:(间隙锁就是BETWEEN 1 到 100区间就是间隙锁,而FOR UPDATE是排他锁的设置方式)
START TRANSACTION;
SELECT * FROM Orders WHERE order_id = BETWEEN 1 AND 100 FOR UPDATE;
COMMIT;
- 防止死锁:在某些情况下,间隙锁可以帮助防止死锁;如果没有间隙锁,那么两个事务可能都会试图在同一位置插入一个新行,导致彼此等待对方释放锁,从而形成死锁;
注意:间隙锁在可重复读(REPEATABLE READ)和序列化(SERIALIZABLE)这两个隔离级别下才会使用,在读已提交(READ COMMITTED)和读未提交(READ UNCOMMITTER)这两个隔离级别下,InnoDB不会使用间隙锁;
例子:
创建一个间隙锁,锁定一个区间:
START TRANSACTION;
SELECT * FROM employee WHERE id > 1 AND id < 4 FOR UPDATE;
COMMIT
查询间隙锁的锁定信息:
SELECT * FROM performance_schema.data_locks;
IX代表意向排他锁;
X是排他锁,代表锁定了2,3,4行的数据(锁定了某个间隙)
当我间隙锁锁定的数据,其他事务都不能使用;
间隙锁的缺陷:
间隙锁(Gap Locks)是MySQL的InnoDB存储引擎用于防止幻读问题的一种锁定机制,虽然它在某些场景下非常有用,但是也存在一些潜在的缺点:
- 性能影响:间隙锁会阻止其他事务在已经锁定的范围内插入新的行,这可能会影响到数据库的并发性能,尤其在需要大量插入操作的高并发场景下;
- 死锁风险:虽然间隙锁可以在某些情况下防止死锁,但在其他情况下,它可能会增加死锁的风险;比如,两个事务都想在同一间隙中插入新的行,因为事务B可能需要拿到事务A的释放锁资源,就可能发生死锁;
- 复杂性:理解间隙锁及其对事务的影响可能需要相当深入的数据库只是,尤其是在处理并发问题和调优数据库性能时;
- 锁定范围可能过大:间隙锁锁定的是索引之间的间隙,这可能会比实际需要锁定的行要多;如果一个事务需要锁定的只是表中的一小部分行,但由于间隙锁的存在,可能会锁定更大范围的数据,导致不必要的锁定冲突;
注意点:以上所述缺点主要取决于具体的使用场景和工作负载,有时候,为了保持数据的一致性和防止并发问题,这些缺点可能是可以接受的;