MySQL中锁概述

为什么需要锁

在MySQL默认的隔离级别(可重复读)下,已经不存在脏读、不可重复读的问题,并且InnoDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了幻读的问题。

那为什么还需要锁机制呢?

来看这种情况:当两个或多个事务选择同一行,然后基于最初选定的值并发更新该行时,由于每个事务都不知道其他事务的存在,就会发生更新丢失问题——最后的更新覆盖了其他事务所做的更新。 所以对于更新丢失这种问题,并不能单靠数据库事务控制器来解决,需要对要更新的数据加必要的锁来解决。

InnoDB中锁的分类

我们知道,InnoDB存储引擎的两个重要特性就是事务管理和行级锁。在MyISAM存储引擎中,只有表锁。这里的表锁和行锁的区别,是指加锁的对象的不同,表锁是对整个表加锁,行锁是对一行或者多行记录进行加锁。行级锁的锁定粒度小,发生锁冲突的概率最低,并发度也最高。

在具体阐述InnoDB中的行锁之前,先将InnoDB中所有锁的类型列出来,然后按照顺序进行解释。

表锁

  • 自增锁(Auto-inc Locks)

  • 意向锁(Intention Locks)

    • 意向共享锁

    • 意向排他锁

行级锁

  • 共享锁

  • 排它锁

  • 间隙锁(Gap Locks)

  • 插入意向锁(Insert Intention Locks)

1、共享锁和排它锁

先看定义:

共享锁(S)

    • 事务拿到某一行记录的共享锁,才可以读取这一行;

    • 事务拿到共享锁后,可以去读加锁的行,同时会阻止其他事务获得相同记录集的排他锁,但其他事务可以同时获取相同记录集的共享锁。

排他锁(X)

    • 事务拿到某一行记录的排它X锁,才可以修改或者删除这一行;

    • 事务拿到排它锁后,可以进行更新操作(update和delete),同时会阻止其他事务取得相同的记录集共享读锁和排他写锁。

最常见的使用共享锁和排它锁的场景:

共享锁:SELECT ... LOCK IN SHARE MODE;

排它锁:SELECT ... FOR UPDATE;

2、意向共享锁和意向排它锁

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

  • 意向共享锁(IS):事务打算给记录行共享锁,事务在给一个记录行加共享锁(S)前必须先取得该表的IS锁。

  • 意向排他锁(IX):事务打算给记录行加排他锁,事务在给一个记录行加排他锁(X)前必须先取得该表的IX锁。

意向锁是InnoDB自动加的,不需要用户干预。

意向锁的作用:解决表级锁和行级锁之间的冲突,比如事务A对表table1中的某条记录加了共享锁,让这一行只能读,不能写。此时,又有事务B申请table1的表锁。如果事务B申请成功,那么理论上它就能修改table1中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。

3、间隙锁

当我们用范围条件而不是相等条件检索数据来请求共享或排他锁时,InnoDB除了会给符合条件的已有记录的索引项加锁,还会对键值在条件范围内但并不存在的记录进行加锁,这种锁机制就是所谓的间隙锁。

举例来说,假如table表中只有101条记录,其id的值分别是1,2,...,100,101,那么如下SQL:

SELECT * FROM table WHERE id > 100 FOR UPDATE

上述SQL中,是一个范围条件的检索,InnoDB不仅会对符合条件的id值为101的记录加锁,也会对id大于101(这些记录并不存在)的“间隙”加锁。InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,如果不使用间隙锁,如果其他事务插入了id大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另一方面,是为了满足其恢复和复制的需要(TODO)。 很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

4、自增锁和插入意向锁

这是两种比较特殊的锁,仅仅在insert的时候会出现。

自增锁

自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

插入意向锁

对已有数据行的修改与删除,必须加排它锁来保证安全,但对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生。插入意向锁是间隙锁(Gap Locks)的一种:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会彼此阻塞。

举个例子,MySQL,InnoDB,默认的隔离级别(RR),假设有数据表:t(id PrimaryKey, name),数据表中有数据:(10, wangwu),(20, zhangsan),(30, lisi),此时:

事务A先执行,还未提交:insert into t values(11, xxx);

事务B后执行:insert into t values(12, ooo);

此时,事务B会不会被阻塞?虽然事务隔离级别是RR,虽然是同一个索引,虽然是同一个区间,但插入的记录并不冲突,所以并不会阻塞事务B。

但假如上面的id列设置了AUTO_INCREMENT,为了保证同一事务中的insert的id自增特性,会使用自增锁,此时,将会阻塞事务B。

其他说明

(1)InnoDB中的行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁;即便在条件中使用了索引字段,但如果MySQL认为全表扫描效率更高,InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

(2)什么时候使用锁

对于UPDATE、DELETEINSERT语句,InnoDB会自动给涉及的记录集加排他锁;

对于普通SELECT语句,InnoDB不会任何锁,除非通过下述两种方式显式地加锁:

# 加共享锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
# 加排它锁
SELECT * FROM table_name WHERE ... FOR UPDATE

由(1)可知,并不是UPDATE、DELETEINSERT语句,就一定是行锁,也有可能是表锁。

(3)死锁的例子:比如,事务A和事务B同时对记录a和记录b进行更新,事务A先对记录a更新(更新操作时InnoDB会对该行自动加锁),然后尝试对记录b更新,事务B先对记录b更新(假如与事务A更新记录a同时进行),然后尝试对记录a更新。此时,两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。

参考

MySQL中的锁(表锁、行锁) MySQL优化系列(八)--锁机制超详细解析(锁分类、事务并发、引擎并发控制) MySQL innodb中各种SQL语句加锁分析 MySQL锁详解 挖坑,InnoDB的七种锁

扩展阅读

MySQL 加锁处理分析

MySQL中的锁(表锁、行锁,共享锁,排它锁,间隙锁)

Last updated