锁的分类
MySQL锁可以按模式分类为:乐观锁与悲观锁。按粒度分可以分为全局锁、表级锁、页级锁、行级锁。按属性可以分为:共享锁、排它锁。按状态分为:意向共享锁、意向排它锁。按算法分为:间隙锁、临键锁、记录锁。
二、全局锁、表级锁、页级锁、行级锁
1. 全局锁
(1) 概念
全局锁就对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的MDL、DDL语句、更新操作的事务提交语句都将被阻塞。
(2) 应用场景
做全库的逻辑备份、全库的导出,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
(3) 实现方式
最常用的全局锁是读锁和写锁。
读锁(共享锁):它阻止其他用户更新数据,但允许他们读取数据。这在你需要在一段时间内保持数据一致性时很有用。
写锁(排他锁):它阻止其他用户读取和更新数据。这在你需要修改一些大量的数据,并且不希望其他用户在这段时间内干扰时很有用。
MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。
当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
风险点:
如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就能停止。
如果在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟。
解决办法:
mysqldump使用参数--single-transaction,启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
2. 表级锁
表级锁会对当前操作的整张表加锁,最常使用的 MyISAM 与 InnoDB 都支持表级锁定。
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
添加表锁,格式如下
lock tables xxx read/write;
例如lock tables t1 read, t2 write; 命令,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能在unlock tables之前访问其他表。
全表更新或者删除:在某些情况下,可能需要对一张表进行全表的更新或者删除操作,例如:删除表中的所有记录,或者更新表中所有记录的某个字段的值。在这种情况下,使用表级锁是合适的。
但要注意,虽然表级锁的开销较小,但由于其锁定粒度大,可能会导致并发度下降,特别是在写操作较多或者并发度较高的场景下。所以,如果应用的并发度较高,或者需要频繁进行写操作,那么可能需要考虑使用更精细粒度的锁,比如说行锁。
元数据锁:MDL 不需要显式使用,在访问一个表的时候会被自动加上,在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
3. 行级锁
行级锁是粒度最低的锁,发生锁冲突的概率也最低、并发度最高。但是加锁慢、开销大,容易发生死锁现象。MySQL中只有InnoDB支持行级锁,行级锁可分为共享锁和排他锁。
在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。 在UPDATE、DELETE操作时,MySQL不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key lock(临键锁)。
注意事项:
行级锁只在事务中有效,也就是说,只有在一个事务开始后并在事务提交火回滚之前,才能对数据进行锁定。如果在非事务环境中指向SQL语句,那么InnDB会在语句执行结束后立即释放所有的锁。
在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。
由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也是会出现锁冲突的。
当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
即便在条件中使用了索引字段,但具体是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
4.意向锁
意向锁是表锁,为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
为什么意向锁是表级锁呢?
当我们需要加一个排他锁时,需要根据意向锁去判断表中有没不有数据行被锁定(行锁)
(1)如果意向锁是行锁,则需要遍历每一行数据去确认;
(2)如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能。
意向锁怎么支持表锁和行锁并存?
(1)首先明确并存的概念是指数据库同时支持表、行锁,而不是任何情况都支持一个表中同
时有一个事务A持有行锁、又有一个事务B持有表锁,因为表一旦但被上了一个表级的写锁,肯
定不能再上一个行级的锁。
(2)如果事务A对某一行上锁,其他事务就不可能修改这一行。这与"事务B锁住整个表就能
修改表中的任意一行"形成了冲突。所以,没有意向锁的时候,让行锁与表锁共存,就会带来
很多问题。于是有了意向锁的出现,如前面所言,数据库不需要在检查每一行数据是否有锁,
而是直接判断一次意向锁是否存在即可,能提升很多性能。
5.间隙锁
间隙锁(Gap Lock)锁定的是索引记录之间的间隙、第一个索引之前的间隙或者最后一个索引之后的间隙。例如,SELECT * FROM t WHERE c1 BETWEEN 1 and 10 FOR UPDATE;会阻止其他事务将 1 到 10 之间的任何值插入到 c1 字段中,即使该列不存在这样的数据;因为这些值都会被锁定。
间隙锁的存在,主要是为了解决幻读问题。所谓幻读,是指在一个事务内读取某个范围的记录
时,另外一个事务在该范围内插入了新的记录,当第一个事务再次读取该范围的记录时,会发
现有些原本不存在的记录,这就是幻读。
举例来说,假设我们有一个存储学生信息的表,有一个事务A要查询年龄在10-20之间的学
生,它在查询前会对这个区间加锁。此时如果有另一个事务3想要插入一个年龄为15的学生,
由于这个年龄的范围已经被事务A锁定,所以事务B必须等待,直到事务A完成,释放锁。这样
就避免了幻读的产生。
值得注意的是,由于间隙锁会锁定范围,如果并发事务较多且涉及的数据范围有交集,可能会
引发性能问题,甚至死锁。因此,在设计数据库和选择隔离级别时,需要综合考虑数据一致性
和并发性能。
间隙锁有什么缺点?
间隙锁(Gap Locks)是MySQL的InnoDB存储引擎用于防止幻读问题的一种锁定机制,虽然
它在某些场景下非常有用,但也存在一些潜在的缺点,包括:
1.性能影响:间隙锁会阻止其他事务在已经锁定的范围内插入新的行,这可能会影响到数据库
的并发性能,尤其在需要大量插入操作的高并发场景下。
2.死锁风险:虽然间隙锁可以在某些情况下防止死锁,但在其他情况下,它可能会增加死锁的
风险。比如,两个事务都想在同一间隙中插入新的行,就可能发生死锁。
3.复杂性:理解间隙锁及其对事务的影响可能需要相当深入的数据库知识,尤其是在处理并发
问题和调优数据库性能时。
4.锁定范围可能过大:间隙锁锁定的是索引之间的间隙,这可可能会比实际需要锁定的行要多。
如果一个事务需要锁定的只是表中的一小部分行,但由于间隙锁的存在,可能会锁定更大范
围的数据,导致不必要的锁定冲突。
请注意,以上所述的缺点主要取决于具体的使用场景和工作负载,有时候,为了保持数据的一
致性和防止并发问题,这些缺点可能是可以接受的。
6.临键锁
临键锁 (Next-Key) 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
例:
id age name
1 10 张三
3 24 李四
5 32 王五
7 45 赵六
该表中`age`列潜在的临键锁有:
(-∞,10]
(10,24]
(24,32]
(32,45]
(45,+∞]
7.记录锁
记录锁是封锁记录,记录锁也叫行锁,例如:
select *from goods where id=1 for update;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。