MySQL InnoDB锁机制

1.背景知识

InnoDB与MyISAM的区别?

  • InnoDB支持事务(TRANSACTION)
  • InnoDB采用了行级锁。行级锁与表级锁本来就有许多不同之处,另外,事务的引入也带来了一些新问题。

事务(Transaction)及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。

  • 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
  • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
  • 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  • 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

为什么InnoDB要有锁机制?

数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外。MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。MySQL各存储引擎使用了三种类型(级别)的锁定机制:表级锁定,行级锁定和页级锁定。

并发事务处理带来的问题

参考:https://blog.csdn.net/shixuetanlang/article/details/80385928

2.InnnoDB锁机制

共享锁(Shared Locks)和排它锁(Exclusive Locks)

InnoDB实现了标准的行锁,分为共享锁(Shared Locks)和排它锁(Exclusive Locks)

  • 共享锁(S):允许事物持有锁,以便读取行数据
  • 排它锁(X):允许事物持有锁,以便更新或删除行数据

如果事物T1在表中的第r行持有一个共享锁,那么另一个事物T2请求第r行的锁,将有如下场景:

  • 如果事物T2请求的是第r行的共享锁,那么将立即获得第r行的共享锁
  • 如果事物T2请求的是第r行的排它锁,那么T2不能理解获得锁。

如果事物T1在第r行加了排它锁,那么事物T2在第r行无论请求加共享锁还是排它锁,都不能立即加上锁。事物T2必须等到T1在第r行的锁释放才能有机会加锁成功。

意向锁(Intention Locks)

InnoDB支持多粒度的锁,其允许行锁和表锁共存。例如:语句LOCK TABLES ... WRITE在一张表上加了排它锁。为了使多粒度锁表可以实现,InnoDB使用了意向锁(Intention Locks)。意向锁是表级别锁,他表明了事物在接下来的处理过程中可能需要用到的行锁类型(Shared Locks or Exclusive Locks).

  • 意向共享锁(IS, intention shared lock)表明事物将要在表的某些行上加共享锁(S)
  • 意向排它锁(IX, intention exclusive lock)表明事物将要在表的某些行上加排它锁(X)
比如 SELECT ... LOCK IN SHARE MODE给表加上了意向共享锁(IS),SELECT ... FOR UPDATE给表加上了意向排它锁(IS)
意向锁的锁协议如下:
  • 在事物获得表中某一行的共享锁(S)之前,该事物必须在该表上加上意向共享锁(IS)或者更强级别的锁
  • 在事物获得表中某一行的排他锁(S)之前,该事物必须在该表上加上意向排它锁(IX)

表级别的锁类型兼容性归纳如下表格所示:

  X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

当一个事物请求的锁与现有的锁相兼容,那么这个事物将可以获得锁,但是如果这个事物想要获取的锁与现在的锁冲突了,那么当前事物是不能立刻获得锁的。这个事物需要等待直到冲突的锁释放之后才能获得锁。如果事物请求获得锁,但是又有可能会出现造成死锁的现象,那么这是MySQL将会抛出一个错误。

除了请求全表以外(如,LOCK TABLES ... WRITE)意向锁几乎不会造成阻塞,意向锁的主要的目标是表明某个线程锁定了某一行或者打算锁定某一行。

InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

  • 在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
  • 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
  • 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
  • 即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

下面通过几个事物的操作,观察一下事物状态和锁状态:

MySQL InnoDB锁机制

  • TransactionA(11691)有2个锁结构(lock struct),锁定了一行。可以推断出一个锁是表级的意向排它锁(IX),一个是行级的排它锁(X)。这个行级的排它锁,会导致后面其他几个事物进行更新操作的时候,等待TransactionA这个排它锁释放后,其他的事物的更新操作才可以在对应的索引行上加锁成功。如果TransactionA始终不释放排它锁(X),那么就会导致后续的其他几个事物等待获取排它锁超时而失败。
  • TransactionB(11692)有个1个锁结构(lock struct),锁定了0行。并且发现,在update的时候,因为获取锁等待而超时了。
  • TransactionC(11693)有个0个锁结构(lock struct),锁定了0行。普通查询操作没有加任何锁。
  • TransactionD(11694)有个1个锁结构(lock struct),锁定了0行。并且发现,在执行for update的时候,因为获取锁等待而超时了。
  • TransactionE(11694)有个1个锁结构(lock struct),锁定了0行。并且发现,在执行lock in share mode的时候,因为获取锁等待而超时了。

Record Locks

单条索引记录上加锁,record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚簇索引,那么锁住的就是这个隐藏的聚簇索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。

Gap Locks

间隙锁是将索引记录之间的间隙加锁或者说是在索引的第一条记录和最后一条记录这段区间上加锁,锁定的记录不包含当前索引记录本身。例如:SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;这条语句就阻止了别的事物在c1=15的这行插入数据,不管c1=15这条数据是否已经存在。这是因为在10-20之间的这段区间全部锁定了。

区间锁可能会覆盖一个索引值,多个索引值甚至是空值。

间隙锁是性能和并发的一个这种方案。

区间锁在带有唯一索引的SQL中是非必需的(不包括多列组成的唯一索引,多列的唯一索引中会发生间隙锁)。例如:如果id是唯一索引,SELECT * FROM child WHERE id = 100;只需要使用索引的Record Locks,而不需要关心是否有别的事物会在id=100之前插入新的值。

当然,如果id不是索引列,或者是非唯一索引,那么SELECT * FROM child WHERE id = 100;将会锁住id=100之前的值。

间隙锁允许多个事物持有相互冲突的锁。例如:事物A在区间G上持有了一个共享锁(S),事物B在区间G上加了一个排它锁(X)。如果索引的某一行被删除了,持有这个间隙锁的所有事物都会合并这个间隙锁。

间隙锁只能起到阻止插入的作用。即:间隙锁只能组织其他的事物往间隙中插入新的值,但是不组织其他的时候获取相同的间隙锁,所以间隙锁跟共享锁功能一样。

间隙锁可以明确的被禁用。如果你把事物的隔离级别改为READ COMMITTED或者允许innodb_locks_unsafe_for_binlog,在这些情况下的查询或者索引扫描,间隙锁都会失效。这时候间隙锁只能用来检查外键约束或者索引唯一性校验。

在使用隔离级别READ COMMITTED或者允许innodb_locks_unsafe_for_binlog时,还有其他一些影响。当MySQL检查了where条件后,会释放不满足条件的那部分索引的记录锁。当执行update操作时,InnoDB执行“半开的一致性”读,这样就可以返回最新提交的数据给到MySQL,MySQL可以决定是否满足update的where条件。

Next-Key Locks

InnoDB在通过查找或者扫描索引的时候,在遇到的索引记录上加上共享锁或者排它锁来实现行锁。所以行锁实际上就是索引的Record Lock。在索引上的Next-Key Locks同样也影响到命中的索引记录之前的区间。Next-Key Locks是索引上的Record Lock和索引值之前的Gap Lock的加成。如果一个回话在索引=R的行上加了共享锁活排它锁,那么其他的回话将立刻不能在索引值<=R的记录上插入数据了。

假设一个索引的值包含10, 11, 13和 20,那么可能的Next-Key Locks包含以下区间:

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]

(20, positive infinity)

默认情况下,InnoDB的事物隔离级别是REPEATABLE READ,并且禁用了innodb_locks_unsafe_for_binlog。在这样的情况下,InnoDB使用next-key locks搜索和遍历索引,从而可以达到防止幻读的效果。

Insert Intention Locks

插入意向锁是一种比为insert操作而设计的优先级比行插入要高的一种Gap Lock。这种锁信号想到达的目的是:多个事物在同一个Gap Lock区间内插入记录,只要不是插入的同一样的话,事物之间就不需要相互等待。假设有个索引,有两个值4和7,不同的事物打算插入5和6这两条记录,每个事物都使用优先级比插入数据的排它锁(X)高的Insert Intention Locks锁定了4至7的区间,那么这些事物之间不会相互影响。

AUTO-INC Locks

AUTO-INC Locks是一种在事物中进行插入使用到了AUTO_INCREMENT列的使用到的一种特殊的锁。例如,如果一个事物正在翔标准插入一条数据,其他事物必须等待,以至于第一个事物插入成功后返回一个递增的主键。

innodb_autoinc_lock_mode配置项控制着AUTO-INC Locks,他让你可以在可预测的连续的序列和最大插入并发量中选择折中方案。