InnoDB的七种锁

 

1. 自增锁(Auto-inc Locks)

2. 共享/排他锁(Shared and Exclusive Locks)

3. 意向锁(Intention Locks)

4. 插入意向锁(Insert Intention Locks)

5. 记录锁(Record Locks)

6. 间隙锁(Gap Locks)

7. 临键锁(Next-Key Locks)

 

相关名词

|--表级锁(锁定整个表)

|--页级锁(锁定一页)

|--行级锁(锁定一行)

|--共享锁(S锁,MyISAM 叫做读锁)

|--排他锁(X锁,MyISAM 叫做写锁)

|--悲观锁(抽象性,不真实存在这个锁)

|--乐观锁(抽象性,不真实存在这个锁)

 

默认事务隔离级别为可重复读(Repeated Read, RR)

 

InnoDB的锁,与索引类型,事务的隔离级别相关

 

自增锁

 

    自增锁是一种特殊的表级别锁(table-level lock)

    从MySQL 5.1开始,InnoDB中提供了一种轻量级互斥量的自增长实现机制

    同时InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式

    进而提高自增长值插入的性能

    

innodb_autoinc_lock_mode和插入类型有关,在介绍它之前,我们先来看看都有哪些插入类型

 

+ **insert-like**  任何会产生新记录的语句,都叫上insert-like,

比如:INSERT,INSERT ...SELECT,REPLACE,REPLACE ...SELECT,and  LOAD DATA,总之包括:simple-inserts,bulk-inserts,mixed-mode inserts.

 

+ **simple inserts**

插入的记录行数是确定的:比如:insert into values,replace

但是不包括: INSERT ... ON DUPLICATE KEY UPDATE.

 

+ **bulk inserts**

插入的记录行数不能马上确定的,比如: INSERT ... SELECT, REPLACE ... SELECT, and LOAD DATA

 

+ **mixed-mode inserts**

这些都是simple-insert,但是部分auto increment值给定或者不给定. 例子如下(where c1 is an AUTO_INCREMENT column of table t1):

INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); 

+ 另外一种 mixed-mode insert 就是 INSERT ... ON DUPLICATE KEY UPDATE

 

#### innodb_autoinc_lock_mode 的说明

 

 

    0 这个表示tradition 传统

    1 这个表示consecutive 连续 (默认)

    2 这个表示interleaved 交错 (MySQL 8.0下默认值为2)

 

1.1 tradition(innodb_autoinc_lock_mode=0) 模式:

  

+ 它提供了一个向后兼容的能力

+ 在这一模式下,**所有的insert语句("insert like")** 都要在语句开始的时候得到一个表级的auto_inc锁,在语句结束的时候才释放这把锁,**注意呀,这里说的是语句级而不是事务级的,** 一个事务可能包含有一个或多个语句。

+ 它能保证值分配的可预见性,与连续性,可重复性,这个也就保证了insert语句在复制到slave的时候还能生成和master那边一样的值(它保证了基于语句复制的安全)。

+ 由于在这种模式下auto_inc锁一直要保持到语句的结束,所以这个就影响到了并发的插入。

 

 

 

1.2 consecutive(innodb_autoinc_lock_mode=1) 模式:

 

+ 对simple insert做了优化,由于simple insert一次性插入值的个数可以立马得到确定,所以mysql可以一次生成几个连续的值,用于这个insert语句,**该值会用互斥量mutex去对内存(dict_table_struct.autoinc)中的计数器进行累加操作。**mysqld重启后,从哪里得到AUTO_INCREMENT呢?内存值肯定是丢失了,实际上MySQL采用执行类似select max(id)+1 from t1;方法来得到AUTO_INCREMENT

+ **Mysql8.0优化:将自增主键的计数器持久化到redo log中。每次计数器发生改变,都会将其写入到redo log中。如果数据库发生重启,InnoDB会根据redo log中的计数器信息来初始化其内存值。**

+ 对于bulk inserts 还是使用传统表锁,该配置下如果不考虑回滚,对于自增列的增长还是连续的。

+ 深入思考:为什么这个模式要产生表级别的锁呢?因为:他要保证bulk insert自增id的连续性,防止在bulk insert的时候,被其他的insert语句抢走auto increment值。

+ 这个模式的好处是auto_inc锁不要一直保持到语句的结束,只要语句得到了相应的值后就可以提前释放锁

 

 

 

1.3 interleaved(innodb_autoinc_lock_mode=2) 模式

 

+ 由于这个模式下已经没有了auto_inc锁,对于所有insert-like 自增长值得产生都是通过互斥量mutex,所以这个模式下的性能是最好的

+ 但是它也有一个问题,就是对于同一个语句来说它所得到的auto_incremant值可能不是连续的,所以基于statement-base replication会出现问题,因此使用这个模式,任何时候都应该使用row-base replication,才能保证最大的并发性能和主从一致性

 

 

    不要没事去更新一个auto_increment列的值,否则自增时,有可能跟你更新的值冲突,导致插入失败

 

共享/排他锁

 

 

    共享/排它锁是标准的行级锁(row-level locking)

 

 

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

 

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

 

3. 多个事务可以拿到一把S锁,读读可以并行;

 

4. 而只有一个事务可以拿到X锁,写写/读写必须互斥;

 

5. 共享/排它锁的潜在问题是,不能充分的并行,解决思路是数据多版本

 

意向锁

 

    意向锁,是一个表级别的锁(table-level locking),但是却表示事务正在读或写某一行记录,而不是整个表。所以意向锁之间不会产生冲突,真正的冲突在加行锁时检查。

    为了方便检测表级锁和行级锁之间的冲突,就引入了意向锁,主要目的是解决表锁和行锁共存的问题。

    

 

1. 意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁

2. 意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁

 

##### 例如

 

```

select ... lock in share mode,要设置IS锁;

select ... for update,要设置IX锁;

```

 

    事务要获得某些行的S锁,必须先获得表的IS锁

    

    事务要获得某些行的X锁,必须先获得表的IX锁

 

+ 意向锁之间互不排斥,但除了 IS 与 S兼容外,意向锁会与共享锁/排他锁互斥

+ **IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突**

+ InnoDB支持多粒度锁,意向锁在保证并发性的前提下,实现了行锁和表锁共存

 

##### 解决的问题

 

+ 如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别意向锁的阻塞。第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。

 

插入意向锁

 

    插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的)

    它是专门针对insert操作的。

    

 

+ 普通的Gap Lock 不允许 在 (上一条记录,本记录) 范围内插入数据

+ 插入意向锁Gap Lock 允许 在 (上一条记录,本记录) 范围内插入数据

+ 如果多个事务插入到相同的索引间隙中,如果它们不在间隙中的相同位置插入,则无需等待其他事务。比如说有索引记录4和7,有两个事务想要分别插入5,6,在获取插入行上的独占锁之前,每个锁都使用插入意图锁锁定4和7之间的间隙,但是不要互相阻塞,因为行是不冲突的,意向锁的涉及是为了插入的正确和高效。

 

##### 插入的过程

 

假设现在有记录 10, 30, 50 且为主键 ,需要插入记录 25 

 

1. 找到 小于等于25的记录 ,这里是 10

2. 找到 记录10的下一条记录 ,这里是 30

3. 判断 下一条记录30 上是否有锁

+ 判断 30 上面如果 没有锁 ,则可以插入

+ 判断 30 上面如果有Record Lock,则可以插入

+ 判断 30 上面如果有Gap Lock/Next-Key Lock,则无法插入,因为锁的范围是 (10, 30) /(10, 30] ;在30上增加insert intention lock( 此时处于waiting状态),当 Gap Lock / Next-Key Lock 释放时,等待的事物( transaction)将被 唤醒 ,此时 记录30 上才能获得 insert intention lock ,然后再插入 记录25

 

4. 注意:一个事物 insert 25 且没有提交,另一个事物 delete 25 时,记录25上会有 Record Lock

 

```

select * from a;

+----+

| a  |

+----+

| 5 |

| 10 |

| 13 |

| 20 |

+----+

```

 

```

//事务A

select * from a where a<=13 for update

```

 

```

//事务B

insert into a values (12)

```

 

```

//事务C

insert into a values (11)

```

事务A不提交,事务B,C被阻塞了,这时候查看

 

```

show engine innodb status\G

```

 

```

mysql tables in use 1, locked 1

LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)

MySQL thread id 3, OS thread handle 140018685810432, query id 240 localhost root update

--等待插入的SQL

insert into a values(12)

------- TRX HAS BEEN WAITING 7 SEC FOR THIS LOCK TO BE GRANTED:

--插入记录12的事物等待中,等待获得插入意向锁(lock_mode X locks gap before rec insert intention waiting)

 

```

 

此时事务A commit

 

 

+ 事务B输出:Query OK, 1 row affected (17.40 sec)

前提条件是insert操作的锁没有超时

+ 事务B未提交,再执行:show engine innodb status\G

 

```

2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1

MySQL thread id 3, OS thread handle 140018685810432, query id 247 localhost root

```

 

    

记录锁

 

    记录锁,它封锁索引记录,例如: select * from t where id=1 for update;

 

+ 记录锁锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。

+ 所以说当一条sql没有走任何索引时,那么将会在每一条聚合索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。

 

 

间隙锁

 

    它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。

    开区间,不包括双端端点

 

```

select * from t 

    where id between 7 and 15 

    for update;

```

 

这个SQL语句会封锁区间,以阻止其他事务id=10的记录插入。

 

    间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”

    如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。

 

临键锁

 

    临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。

    左闭右开区间

 

+ 但当查询唯一索引的时候,且记录存在,Next-Key Lock 会进行优化,将其降级为RecordLock,即仅锁住索引本身,不是范围。

+ 但当查询唯一索引的时候,且记录不存在,使用Gap Lock

+ 但当查询唯一索引的时候,使用范围查询 > <,使用Gap Lock + Record Lock,锁上界,不锁下界

 

 

    临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

 

 

### 总结:

 

    InnoDB使用共享锁,可以提高读读并发;

    排他锁,为了保证数据强一致,InnoDB使用强互斥锁,保证同一行记录修改与删除的串行性;

    InnoDB使用插入意向锁,可以提高插入并发;

    记录锁锁定索引记录;

    间隙锁锁定间隔,防止间隔中被其他事务插入;

    临键锁锁定索引记录+间隔,防止幻读;

 

普通select

加锁select

update与delete

insert

各类SQL语句分别加了什么锁?

 

select

 

普通的select是快照读,而select ... for update或select ... in share mode则会根据情况加不同的锁

 

如果在唯一索引上用唯一的查询条件时( where id=1),加记录锁

 

否则,其他的查询条件和索引条件,加间隙锁(BETWEEN AND )或Next-Key 锁(可重复隔离级别)

 

update与delete

 

如果在唯一索引上使用唯一的查询条件来update/delete,加记录锁

 

否则,符合查询条件的索引记录之前,都会加Next-Key 锁

 

注:如果update的是聚集索引,则对应的普通索引记录也会被隐式加锁,这是由InnoDB索引的实现机制决定的:普通索引存储PK的值,检索普通索引本质上要二次扫描聚集索引。

 

insert

 

insert和update与delete不同,它会用排它锁封锁被插入的索引记录,同时,会在插入区间加插入意向锁,但这个并不会真正封锁区间,也不会阻止相同区间的不同KEY插入。

 

### 查看锁等待情况

 

```

select * from information_schema.innodb_locks;

select * from information_schema.innodb_lock_waits;

select * from information_schema.innodb_trx;

```

 

##### 显示锁 vs 隐示锁

 

 

1. 显示锁(explicit lock)

    显示的加锁,在show engine innoDB status 中能够看到  ,会在内存中产生对象,占用内存

    eg: select ... for update , select ... lock in share mode

 

2. 隐示锁(implicit lock)

    implicit lock 是在索引中对记录逻辑的加锁,但是实际上不产生锁对象,不占用内存空间

 

3. 哪些语句会产生implicit lock 呢?

   eg: insert into xx values(xx)

   eg: update xx set t=t+1 where id = 1 ; 会对辅助索引加implicit lock

 

4. implicit lock 在什么情况下会转换成 explicit lock

  eg: 只有implicit lock 产生冲突的时候,会自动转换成explicit lock,这样做的好处就是降低锁的开销

  eg: 比如:我插入了一条记录10,本身这个记录加上implicit lock,如果这时候有人再去更新这条10的记录,那么就会自动转换成explicit lock

 

5. 数据库怎么知道implicit lock的存在呢?如何实现锁的转化呢?

  1. 对于聚集索引上面的记录,有db_trx_id,如果该事务id在活跃事务列表中,那么说明还没有提交,那么implicit则存在

  2. 对于非聚集索引:由于上面没有事务id,那么可以通过上面的主键id,再通过主键id上面的事务id来判断,不过算法要非常复杂,这里不做介绍

 

##### metadata lock

 

1. 这是Server 层实现的锁,跟引擎层无关

2. 当你执行select的时候,如果这时候有ddl语句,那么ddl会被阻塞,因为select语句拥有metadata lock,防止元数据被改掉