由于我们一直讨论的InnoDB引擎默认的事务级别是“可重复度”(Repeatable Read),所以为了避免幻读,InnoDB还会在每一个排它性行锁周围都加上间隙锁(GAP)。那么在这个事务级别下表锁最终的逻辑表现就如下图所示:
是的,没有索引可以提供检索依据的数据表正在进行一场豪赌!这还是只有13条数据的情况下,那么试想一下如果数据表中有10,000,000条记录呢?这不仅造成资源的浪费,更重要的是表锁是造成死锁的重要原因,而且由此引发的InnoDB自动解锁代价非常昂贵(后文会详细讲到)。
4-3-3、死锁一旦构成死锁,InnoDB会尽可能的帮助开发者解除死锁。其做法是自动终止一些事务的运行从而释放锁定状态。在上一小节我们示范的多个加锁场景,它们虽然都构成锁等待,但是都没有构成死锁。那么本文就要首先说明一下,什么样的情况才构成死锁。
4-3-3-1、什么是死锁两个或者多个事务相互等待对方已锁定的资源,而彼此都不为协助对方达成操作目而主动释放已锁定的资源,这样的情况就称为死锁。请区分正常的锁等待和死锁的区别,例如以下示意图中的锁等待并不构成死锁:
上图中的情况只能称为锁资源等待,这是因为当A事务完成处理后就会释放所占据的资源上的锁,这样B事务就可以继续进行处理。并且在这个过程中没有任何因素阻止A事务完成,也没有任何因素阻止B事务在随后的操作中获取锁。但是,以下示意图中的两个事务就在相互等待对方已锁定的资源,这就称为死锁:
上图中A事务已为id1和id2这两个索引项加锁,当它准备为id4这个索引加锁时,却发现id4已经被事务B加锁,于是事务A进行等待过程。恰巧的是,B事务在为id4、id5加锁后,正在等待为id2这个索引项加锁。于是最后造成的结果就是事务A和事务B相互等待对方释放资源。注意,由于需要保证事务的ACID特性,所以A事务已经锁定的索引id1、id2在事务A的等待过程中,是不会被释放的;同样事务B已经锁定的索引id4、id5在等待过程中也不会被释放。很明显如果没有外部干预,这个互相等待的过程将一直持续下去。这就是一个典型的死锁现象。在实际应用场景中,往往会由超过两个事务共同构成死锁现象,甚至会出现强制终止某一个等待的事务后依然不能解除死锁的复杂情况。
4-3-3-2、死锁出现的原因死锁造成的根本原因和上层MySQL服务和下层InnoDB引擎的协调方式有关:在上层MySQL服务和下层InnoDB引擎配合进行Update、Delete和Insert操作时, 对满足条件的索引加X锁的操作是逐步进行的。
当InnoDB进行update、delete或者insert操作时,如果有多条记录满足操作要求,那么InnoDB引擎会锁定一条记录(实际上是相应的索引)然后再对这条记录进行处理,完成后再锁定下一条记录进行处理。这样依次循环直到所有满足条件的数据被处理完,最后再统一释放事务中的所有锁。如果这个过程中某个将要锁定的记录已经被其它事务抢先锁定,则本事务就进入等待状态,一直等待到锁定的资源被释放为止。
要知道在正式的生成环境中,可能会同时有多个事务对某一个数据表上同一个范围内的数据进行加锁(加X锁后进行写操作)操作。而InnoDB引擎和MySQL服务的交互采用的这种方式很可能使这些事务各自持有某些记录的行锁,但又不构成让事务继续执行下去的条件。那为什么说在生产环境下,多数死锁状态的出现是因为表锁导致的呢?
首先,表锁本身并不会导致死锁,它只是InnoDB中的一种机制。但是表锁会一次锁定数据表中的所有聚集索引项。这就增加了表锁所在事务需要等待前序事务执行完毕才能继续执行的几率。而且这种等待状态还很可能在一个事务中出现多次——因为有多个事务在同时执行嘛。在这个过程中由于表锁逐渐占据了聚簇索引上绝大多数的索引项,所以这又增加了和其它正在执行的事务抢占锁定资源的,最终增加了死锁发生的几率。