阻塞
阻塞情况是因为不同锁之间的兼容性关系,事务的执行可能要需要等待锁的释放才可以继续进行下去,那么在这个时间段就称为阻塞。
阻塞其实并不是一件坏事,因为阻塞是为了确保事务可以并发且正常地运行,保证数据的一致性。
InnoDB使用innodb_lock_wait_timeout用来控制等待的时间(默认是50S),超过这个时间就不等待了,是要结束事务还是要回滚取决于innodb_rollback_on_timeout(默认为off),该用来设定是否在等待超时后,是否要对进行中的事务进行回滚操作。
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SHOW VARIABLES LIKE 'innodb_rollback_on_timeout';
//进行等待时间,设为60S
SET @@innodb_lock_wait_timeout=60;
SET @@innodb_rollback_on_timeout=on;


在默认情况下,InnoDB存储引擎不会回滚超时的异常(也就是innodb_rollback_on_timeout默认为off)。
死锁
死锁是指两个或两个以上的事务在执行过程中,因为争夺锁资源而造成一种互相等待的现象
解决死锁的方式
解决死锁的问题最简单粗暴额方法就是全部事务进行回滚,让事务重新开始,但这样会导致并发性能的下降,甚至任何一个事务都不能够进行。
解决死锁另一个方式就是一个超时,即当一个事务等待的时间超过设置的某一阈值,那么这个事务就要回滚,并释放自己的资源,然后另一个事务就可以拿到资源了,就可以继续进行下去。
虽然超时机制比较简单,根据FIFO原则(先进先出)选择回滚对象,即先进行的事务会优先进行回滚,但这种简单的选择方式会消耗更多的性能,比如要回滚的事务是业务量很大的,或者业务的权重占比比较大,或者已经更新了很多行,产生了比较多的undo页,那么选择这个事务去进行回滚就不太合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多
因此,除了超时机制,InnoDB存储引擎采用的**等待图(wait-for graph)**的方式去进行死锁检测
等待图
wait-for graph需要数据库去保存下面的两种信息
- 锁的信息链表
- 事务等待链表
事务等待链表存储的是事务的等待顺序,即进入阻塞的链表。
锁的信息链表记录的是该行的各种锁之后要被占用的信息,相当于行程规划。
举个栗子

Transacting Wait Lists里面的事务指向row里面的锁,表示事务正在等待指向的锁以上的那些锁的释放
比如T1指向了row1的S锁,代表T1想要row1的S锁,但需要等T2将row1的X锁释放。
所以上面这幅图,总共有T1、T2、T3在等待锁
- T1在等待T2释放row1的X锁
- T2在等待T4释放row2的S锁,等待T1释放row2的S锁
- T3在等待T2释放row2的X锁,等待T4释放row2的S锁,等待T1释放row2的S锁
那么生成的wait-for graph图如下所示

只要wait-for graph图中出现回路,就代表出现了死锁情况,很明显,上图中T1和T2就形成了回路,生成了一个死锁。
wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则代表出现了死锁,一般来说,InnoDB会选择回滚undo量最小的事务
拓展:为何外键无索引会导致死锁发生
假设有A事务,往子表中增加数据会修改数据,那么它需要子表的行锁(X锁)或表锁(X锁,可能根据外键修改),父表的行锁(该行锁是在外键索引中的),因为外键没有索引,那么就会升级成表锁,那么就需要等待别的事务释放表锁,而表锁的释放比行锁麻烦,因为表锁是意向锁,表示对该表访问一些数据,或者修改一些数据,而不是像行锁,行锁只要对行数据操作完了就会释放。
锁升级
锁升级是指将当前锁的粒度升级,也就是变得更粗,比如从行锁升级成页锁,然后升级成表锁。
锁升级可以减少锁的开销,锁的粒度越低,那么锁就越多,对应的开销也就越大,所以升级锁在一定程度上提高了效率
锁升级虽然减少了开销,但会导致并发性能的降低。
一般来说,InnoDB都是行锁、间隙锁,只有在缺失索引的条件下才会升级成表锁,也就是意向锁。
本文介绍了InnoDB存储引擎中的阻塞与死锁现象,详细解释了等待图(wait-for graph)检测机制及锁升级的概念。通过具体案例展示了如何通过调整参数来避免长时间的阻塞,并探讨了死锁发生的原因。
554

被折叠的 条评论
为什么被折叠?



