# 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锁，才可以修改或者删除这一行；
  * 事务拿到排它锁后，可以进行更新操作（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：

```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、DELETE`和`INSERT`语句，InnoDB会自动给涉及的记录集加排他锁；

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

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

由（1）可知，并不是`UPDATE、DELETE`和`INSERT`语句，就一定是行锁，也有可能是表锁。

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

## 参考

[MySQL中的锁（表锁、行锁）](http://www.cnblogs.com/chenqionghe/p/4845693.html)\
[MySQL优化系列（八）--锁机制超详细解析（锁分类、事务并发、引擎并发控制）](http://blog.csdn.net/jack__frost/article/details/73347688)\
[MySQL innodb中各种SQL语句加锁分析](http://www.fordba.com/locks-set-by-different-sql-statements-in-innodb.html)\
[MySQL锁详解](http://www.cnblogs.com/luyucheng/p/6297752.html)\
[挖坑，InnoDB的七种锁](https://mp.weixin.qq.com/s/IE31GSDP0Ndjzc8kFj4Ukw)

**扩展阅读**

[MySQL 加锁处理分析](http://hedengcheng.com/?p=771)

[MySQL中的锁（表锁、行锁，共享锁，排它锁，间隙锁）](https://blog.csdn.net/soonfly/article/details/70238902)
