什么是锁

MySQL中为了保证数据访问的一致性与有效性等功能,实现了锁机制,MySQL中的锁是在服务器层或者存储引擎层实现的。

锁是用来解决什么问题的

锁是用来解决并发事务的访问问题,我们已经知道事务并发执行时可能带来的各种问题,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事务进行读取操作,另一个同时进行改动操作的情况下。

一个事务进行读取操作,另一个进行改动操作,我们前边说过,这种情况下可能发生脏读、不可重复读、幻读的问题。

什么是脏读,不可重复读,幻读

脏读

脏读(Dirty Read)是指在数据库事务中,一个事务读取了另一个并发事务尚未提交的数据。当一个事务读取了另一个未提交事务的数据后,如果该未提交事务最终回滚,则读取到的数据实际上是不正确的,即读取到了"脏数据"。

MySQL中,默认的事务隔离级别是可重复读(REPEATABLE READ),在该隔离级别下,脏读是被禁止的。然而,在低隔离级别如读已提交(READ COMMITTED)下,脏读是可能发生的。

举个例子来说明脏读的情况:

  • 事务A开始,读取一行数据的值为X。
  • 事务B在事务A未提交的情况下修改了该行数据的值为Y。
  • 事务A继续读取同一行数据,此时读取到的数据值为Y,即脏数据。
  • 如果事务B最终回滚,则事务A读取到的数据是错误的。

脏读可能导致数据不一致和逻辑错误,因此在设计数据库应用程序时需要注意处理并发访问的问题。可以通过提高事务隔离级别、使用锁机制或乐观并发控制等方式来避免脏读的发生。

不可重复读

不可重复读(Non-repeatable Read)是数据库事务隔离级别中的一种现象,指在同一个事务中,多次读取同一数据时,得到不一致结果的情况。简而言之,不可重复读是指在同一个事务内,由于其他事务的并发操作导致了数据的不一致性。

在数据库中,事务隔离级别用于控制事务之间的可见性和并发操作的行为。在较低的隔离级别下,比如读已提交(Read Committed)级别,就可能出现不可重复读现象。

具体来说,不可重复读可能发生在以下情况下:

  1. 一个事务第一次读取了某个数据。
  2. 在事务进行中,另一个事务修改并提交了该数据。
  3. 事务再次读取相同的数据,发现数据不一致,与第一次读取到的值不同。

不可重复读的存在可能导致事务在多次读取同一数据时获得不一致的结果,这可能会对事务的逻辑产生影响。

为了避免不可重复读,可以使用较高的事务隔离级别,如可重复读(Repeatable Read)或串行化(Serializable),或者通过锁机制来保证数据的一致性和隔离性。

幻读

幻读(Phantom Read)是数据库事务隔离级别中的一种现象,指在同一个事务中,多次执行相同的查询条件,得到不同的行数结果的情况。简而言之,幻读是指在同一个事务内,由于其他事务的并发插入或删除操作导致了数据行的变动。

在数据库中,事务隔离级别用于控制事务之间的可见性和并发操作的行为。幻读通常发生在较低的隔离级别下,如读已提交(Read Committed)和可重复读(Repeatable Read)。

具体来说,幻读可能发生在以下情况下:

  1. 一个事务第一次执行了某个查询,并获取了一些数据行。
  2. 在事务进行过程中,另一个事务插入了符合第一次查询条件的新数据行。
  3. 事务再次执行相同的查询,发现有新增的数据行出现,就好像出现了幻觉一样。

幻读的存在可能导致事务在同一个查询中获得不同的数据行数,这会对事务的逻辑产生影响。

为了避免幻读,可以使用更高的事务隔离级别,如串行化(Serializable),或者通过锁机制或乐观并发控制机制来保证查询的一致性和隔离性。

锁的分类

模式分类

乐观锁

乐观锁(Optimistic Lock)是一种基于冲突检测的并发控制机制。与悲观锁不同,乐观锁在执行操作时,不会对资源进行加锁,而是通过版本号或时间戳等方式,在没有发生冲突的情况下进行更新。

具体来说,在应用使用乐观锁机制时,需要为数据表增加一个额外的版本号或时间戳列。在读取数据时,将版本号或时间戳值一并读出,并将其记录到客户端。当客户端准备提交修改时,再将之前读取的版本号或时间戳值发送到数据库服务器端;数据库服务器端检查当前的版本号或时间戳是否与客户端提交的一致,如果一致,则说明数据没有被其他事务修改,可以进行提交操作;否则,说明数据已被其他事务修改,需要回滚事务或重试。

以时间戳为例,具体实现过程如下:

  1. 在数据表中添加一个时间戳列;
  2. 在读取数据时,将时间戳值一并读出,并将其记录到客户端;
  3. 客户端在更新操作时,将之前读取的时间戳值加入WHERE条件中,如UPDATE table SET column = value, timestamp = new_timestamp WHERE id = x AND timestamp = old_timestamp;
  4. 数据库服务器端执行更新操作时,会检查该行的时间戳是否等于客户端提交的时间戳,如果等于,则允许更新,否则拒绝更新。

乐观锁的优点是不会阻塞其他事务的读取操作,而且在并发量较低的情况下,性能较好。同时,乐观锁避免了死锁等问题,降低了并发冲突的概率。不过,乐观锁也存在一定的缺点,由于需要频繁进行版本号或时间戳的比较,可能存在较高的开销。此外,乐观锁只适用于少量并发更新操作的场景,当并发度较高时,冲突的概率会增加,乐观锁的性能优势可能会被削弱。

悲观锁

MySQL中的悲观锁(Pessimistic Lock)是一种基于加锁的并发控制机制,用于保证在事务处理期间所操作的数据不会被其他事务修改。悲观锁在执行操作时,会对操作的数据进行加锁,阻塞其他事务的修改操作,等待自己完成操作后释放锁。

具体来说,MySQL中的InnoDB存储引擎通过行级锁实现悲观锁。当开启事务并执行SELECT语句时,InnoDB会自动给查询结果集中的每条记录都添加共享锁(Shared Lock),其他事务可以同时读取这些记录,但不能进行写入操作。当需要更新或删除记录时,InnoDB会将要修改的记录加上排他锁(Exclusive Lock),其他事务无法访问该记录,直到当前事务释放锁为止。如果多个事务同时请求同一条记录的排他锁,则会产生死锁。

悲观锁的优点是能够保证数据的一致性和完整性,避免出现脏读、不可重复读和幻读等问题。但是,由于加锁和解锁需要频繁地进行上下文切换,带来较大的性能开销,且可能导致数据库的并发性能下降。因此,在设计数据库系统时,应根据实际情况选择适当的锁机制,平衡数据的一致性和性能。

粒度分类

全局锁

全局锁是MySQL中的一种锁机制,可以对整个数据库实现加锁操作,阻止其他事务对数据库进行修改。与行级锁不同,全局锁是对整个数据库进行加锁,可以用于备份、数据迁移等操作,确保在进行这些操作时,没有其他事务对数据库进行修改。

全局锁在MySQL中通过命令“FLUSH TABLES WITH READ LOCK”实现,该命令会将所有MySQL表加上只读锁(READ LOCK),避免其他事务对表进行修改。在此状态下,只能执行查询操作,不能执行更新和删除操作,直到解锁为止。可以使用命令“UNLOCK TABLES”释放全局锁。

需要注意的是,全局锁会导致所有的事务请求都被阻塞,因此,在使用全局锁进行备份或数据迁移等操作时,应尽量选择低负载的时间段进行操作。同时,全局锁也可能会影响系统的性能,因此,在使用全局锁时需要慎重考虑。如果只是需要对一部分表进行备份或迁移,可以考虑使用表级锁或行级锁等更细粒度的锁机制。

表级锁

MySQL中的表级锁是一种锁机制,用于对整个表进行加锁和解锁操作。当一个事务获取了表级锁后,其他事务无法对该表进行修改操作,直到持有锁的事务释放锁。

MySQL中有两种常见的表级锁:

  1. 表共享读锁(Table Read Lock):也称为表共享锁(Table Shared Lock),多个事务可以同时获得表的共享读锁,允许并发地读取表的数据,但禁止对表进行写入操作。其他事务对该表的读请求不会被阻塞,因为读操作之间不会互斥。只有当有事务持有共享读锁时,其他事务才无法对表进行写入操作。
  2. 表排他写锁(Table Write Lock):也称为表排他锁(Table Exclusive Lock),当一个事务获得表的排他写锁后,其他事务无法对该表进行任何读写操作,必须等待持有锁的事务释放锁。只有当没有事务持有排他写锁时,其他事务才能获取并发地读取或修改表的数据。

使用表级锁需要注意以下几点:

  • 表级锁是粗粒度锁,会导致并发性能较差,在高并发场景下可能引起性能问题。
  • 表级锁是自动加锁的,无需手动操作,MySQL会根据提交事务的需求自动进行加锁。
  • 表级锁的范围是整个表,对表中的所有行起作用。
  • 表级锁会阻塞其他事务的读写操作,可能导致死锁问题,因此需要合理控制锁的粒度和持有时间。

在实际应用中,可以根据业务需求和并发情况选择适当的锁机制,例如使用行级锁、乐观锁等来提高并发性能。        

页局锁

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 引擎支持页级锁。

行局锁

行级锁是 MySQL 粒度最细的锁,发生锁冲突概率最低,但是加锁慢,开销大。MySQL 中只有 InnoDB 引擎支持行锁,其他不支持。

属性分类

共享锁

MySQL中的共享锁(Shared Lock)是一种行级锁,也称为读锁。共享锁允许多个事务同时获取同一行的锁,用于并发读取操作。

当一个事务获取了某一行的共享锁之后,其他事务也可以获取该行的共享锁,这些事务可以并发地读取相同的数据行,互不干扰。共享锁之间不会互相阻塞,它们只会阻塞排他锁(Exclusive Lock)。

共享锁在事务读取数据时使用,用于确保数据的一致性和并发性。多个事务可以同时读取同一行数据,这样可以提高并发读取的效率。

需要注意的是,共享锁与排他锁之间存在冲突。当一个事务持有共享锁时,其他事务无法获取该行的排他锁,只有等待当前的共享锁释放后才能获取排他锁。这种机制保证了共享锁事务和修改数据的排他锁事务之间的互斥性。

共享锁的特点如下:

  • 共享锁可以被多个事务同时持有。
  • 共享锁之间不会相互阻塞,允许并发读取。
  • 共享锁与排他锁之间存在冲突,共享锁会阻塞排他锁的获取。

共享锁在MySQL中的应用场景包括读取操作、查询操作,以及对数据进行共享访问的情况。例如,多个事务需要同时读取某一行数据,可以通过获取共享锁实现并发读取,保障数据的一致性和并发性。

排他锁

MySQL中的共享排他锁(Shared and Exclusive Locks)是一种混合形式的锁,也称为读写锁。

共享排它锁结合了共享锁和排它锁的特性,既允许多个事务同时以共享模式访问同一行数据,又保证同时只有一个事务能以排它模式进行修改操作。共享锁的特性在查询操作上非常有效,而排它锁的特性则在更新操作上非常有用。

在MySQL中,当一个事务请求获取某一行数据的共享排它锁时,如果当前没有任何其他事务正在修改该行数据(即没有任何事务持有排它锁),那么该事务会立即获取到该行的排它锁,并且可以进行更新操作。如果已经有其他事务持有共享锁,那么该事务将得到一个共享锁,并在其他事务释放共享锁后再重新请求排它锁进行修改操作。这种机制可以提高并发性,同时也保护了数据的完整性。

需要注意的是,共享排它锁是MySQL的InnoDB存储引擎中实现的,不是所有的存储引擎都支持这种锁机制。此外,不同的数据库系统可能有不同的锁机制和实现方式。

共享排它锁的特点如下:

  • 允许多个事务以共享模式同时读取同一行数据。
  • 保证同时只有一个事务可以以排它模式进行修改操作。
  • 共享锁与排它锁之间存在冲突,排它锁会阻塞共享锁的获取。

共享排它锁在MySQL中的应用场景包括大量读操作和一定程度的写操作,例如,大量并发读取某一数据表的情况下,一定程度上允许对该表进行更新操作。

状态分类

意向共享锁

意向共享锁(Intention Shared Lock)是MySQL中一种特殊的锁,用于在表级别上表示事务随后将获取一个或多个行级别的共享锁。用意向共享锁可以避免死锁。

当一个事务请求获取某一行数据的共享锁时,它可能需要在该表上先获取一个意向共享锁。如果该表上已经有其他事务持有排它锁,则新事务无法获取意向共享锁,从而避免了死锁的发生。如果没有任何其他事务持有排它锁,那么新事务可以获取意向共享锁,并继续获取行级别的共享锁。

需要注意的是,意向共享锁是一种协调锁,不会阻塞任何其他的读请求,包括意向共享锁和共享锁。它只是告诉其他事务,当前事务有意向获取共享锁,让它们知道接下来可能有共享锁的请求。

意向共享锁可以提高并发性,减少死锁的发生。在MySQL的InnoDB存储引擎中,每个表都有两种类型的意向锁:意向共享锁和意向排它锁。它们的含义如下:

  • 意向共享锁(IS):表示事务后续将获取一些共享锁。
  • 意向排它锁(IX):表示事务后续将获取一个排它锁。

意向锁的特点如下:

  • 意向锁不会直接阻塞其他事务的读请求,它只是告诉其他事务当前事务可能会产生共享锁或排它锁。
  • 意向共享锁避免了死锁的发生,可以提高并发性。

意向共享锁在MySQL中的应用场景包括并发读取数据表、并发插入数据等情况。

意向排他锁

意向排他锁(Intention Exclusive Lock)是MySQL中一种特殊的锁,用于在表级别上表示事务随后将获取一个或多个行级别的排它锁。用意向排它锁可以避免死锁。

当一个事务请求获取某一行数据的排它锁时,它可能需要在该表上先获取一个意向排它锁。如果该表上已经有其他事务持有任何类型的锁(包括共享锁、意向共享锁和意向排它锁),则新事务无法获取意向排它锁,从而避免了死锁的发生。如果没有任何其他事务持有锁,那么新事务可以获取意向排它锁,并继续获取行级别的排它锁。

需要注意的是,意向排它锁是一种协调锁,不会阻塞任何其他的读请求,包括意向共享锁和共享锁。它只是告诉其他事务,当前事务有意向获取排它锁,让它们知道接下来可能有排它锁的请求。

意向排它锁可以提高并发性,减少死锁的发生。在MySQL的InnoDB存储引擎中,每个表都有两种类型的意向锁:意向共享锁和意向排它锁。它们的含义如下:

  • 意向共享锁(IS):表示事务后续将获取一些共享锁。
  • 意向排它锁(IX):表示事务后续将获取一个排它锁。

意向锁的特点如下:

  • 意向锁不会直接阻塞其他事务的读请求,它只是告诉其他事务当前事务可能会产生共享锁或排它锁。
  • 意向排它锁避免了死锁的发生,可以提高并发性。

意向排它锁在MySQL中的应用场景包括大量更新数据、并发插入数据等情况。

算法分类

间隙锁

MySQL中的间隙锁(Gap Lock)是一种锁机制,用于在事务中防止其他事务插入到已存在的间隙(索引范围)中。它的作用是保证数据的完整性和一致性。

在MySQL中,如果事务使用WHERE条件查询一个范围的数据,并请求行级别的锁,那么MySQL会对这个范围内的每一行进行加锁。但是,如果该范围内有未被索引的间隙(即不存在的记录),为了避免其他事务在这个间隙中插入新的记录,MySQL会生成间隙锁。这样就可以确保其他事务无法在这个间隙中插入新的记录,从而保护数据的一致性。

间隙锁的特点如下:

  • 间隙锁是在索引范围内的间隙上加的锁,而不是指定的行上。
  • 间隙锁可以阻止其他事务在这个间隙中插入新的记录,从而保证了数据的完整性和一致性。
  • 间隙锁只与读一致性锁(例如共享锁)和写锁(例如排它锁)产生冲突,与其他的间隙锁不产生冲突。
  • 间隙锁在事务提交或回滚后自动释放。

间隙锁在MySQL中的应用场景常见于多个事务同时操作同一个范围的数据,通过使用间隙锁可以确保数据的一致性和避免冲突。

需要注意的是,间隙锁在某些情况下可能会导致性能问题,因为它会限制其他事务的并发性。因此,在实际应用中,需要谨慎使用间隙锁,并结合具体场景进行性能优化。

临键锁

临键锁(Next-Key Lock):临键锁是查询时InnoDB根据查询的条件而锁定的一个范围,这个范围中包含有间隙锁和记录数;临键锁=间隙锁+记录锁。
其设计的目的是为了解决Phantom Problem(幻读);主要是阻塞insert,但由于临键锁中包含有记录锁,因此临键锁所锁定的范围内如果包含有记录,那么也会给这些记录添加记录锁,从而造成阻塞除insert之外的操作;
Tips:临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
临键锁锁住的区间为:记录+区间(左开右闭)
左开右闭:不锁住左边,锁右边

记录锁

在MySQL中,记录锁(Record Lock)是一种行级别的锁机制,用于保护数据库表中的单个记录或行。当一个事务对某个记录进行操作时(如读取、更新或删除),MySQL会自动为该记录加上记录锁,以确保数据的一致性和并发控制。

记录锁的特点如下:

  • 记录锁是针对表中的单个记录或行加的锁。
  • 记录锁是排他锁(也称为写锁),可防止其他事务同时修改同一条记录。
  • 当一个事务对某个记录加上记录锁后,其他事务要修改(包括读取、更新、删除)同一条记录时,会被阻塞,直到持有该记录锁的事务释放锁。
  • 记录锁在事务提交或回滚后自动释放。

记录锁在并发环境中起着重要的作用,它保证了数据的一致性和隔离性。通过记录锁,可以避免多个事务同时对同一条记录进行修改,从而防止数据不一致或冲突的情况发生。

需要注意的是,使用记录锁会对并发性能产生一定影响,过多的锁竞争可能导致性能下降。因此,在设计数据库应用时,需要合理使用锁机制,以平衡数据一致性和并发性能。 ​