前言
一个僵持锁(deadlocks)是指锁处于僵持的状态,持有锁的事务既得不到期望的资源,也不愿意释放其他事务需要的资源,也就是,多个锁相互之间都持有其他锁所需的资源,所有的事务都在等待各自需要的可用的资源,但是所有的事务都不会释放各自持有的锁,从而引起持有这些锁的事务僵持不下,无法再向前执行事务。
当多个事务(以语句UPDATE or SELECT ... FOR UPDATE),以相反的方向,锁住多个数据表的多个行记录的时候,容易发生僵持锁的情况。当多个事务锁住一定范围内的索引记录以及索引记录之间的间隙的时候,并且这些事务需要的资源相同,由于时间性的原因(事务执行时间太长),容易发生僵持锁的情况。
为了降低发生僵持锁的可能性,使用事务代替LOCK TABLES语句,并且保证事务执行插入或者更新操作的数据量足够小,而不会消耗太多的时间。当需要执行大数量的更新的时候,保持所有事务在相同的顺序中执行,执行语句SELECT ... FOR UPDATE 或者语句UPDATE ... WHERE的数据表需要对操作的列建立索引。由以上的分析可知,僵持锁不会受到事务隔离级别的影响,因为,事务隔离级别只是约束读操作的行为,而僵持锁的发生是与写操作的执行顺序有关。
当僵持锁的探测功能开启(默认是开启的)以及僵持锁发生的时候,InnoDB探测执行语句的条件并且对发生僵持锁的事务执行回滚操作,如果属性选项innodb_deadlock_detect的僵持锁探测功能关闭,则InnoDB使用属性选项innodb_lock_wait_timeout提供的超时设置,当锁僵持的时间超出限制,对发生僵持锁的事务执行回滚操作。
僵持锁示例
假设,存在两个客户端A、B,其执行的流程如下所示,首先,客户端A创建两个数据表Animals、数据表Birds,并且对这些表插入一些数据,随后,客户端A开启一个事务,以共享锁的模式对数据表Animals执行查询操作:
随后,客户端B开启一个事务,以共享锁的方式对数据表Birds执行一个查询操作,如下所示:
随后,客户端B对数据表Animals执行更新操作,如下所示:
同时,客户端A对数据表Birds 执行更新操作,此时,发生僵持锁,如下所示:
查看InnoDB的获取僵持锁的统计信息,如下所示:
僵持锁探测
如前面所述,InnoDB是默认开启自动探测僵持锁的功能,一旦出现僵持锁,则回滚持有僵持锁的事务,如果自动探测僵持锁的功能是关闭的,则InnoDB提供超时机制,一旦僵持锁的持续时间超出时间的限制,则自动回滚持有僵持锁的事务。在回滚僵持锁的时候,InnoDB将选择较小的事务执行执行回滚操作,例如,处理数据量小的事务。
僵持锁处理
合理组织事务中语句的执行顺序能够降低出现僵持锁的几率,提供以下的方法:
-
实时监控数据库的执行情况,使用语句SHOW ENGINE INNODB STATUS监控发生僵持锁的日志信息,实时分析以及及时做出对策与调整。
-
如果运行环境中频繁地出现僵持锁的情况,则开启innodb_print_all_deadlocks输出MySQL的错误日志信息,待僵持锁的问题已解决,则关闭调试日志的输出以免影响MySQL服务的运行性能。
-
由于MySQL提供自动回滚僵持锁的事务,待MySQL回滚事务完成,则业务应用可以提供方法重试提交出现僵持锁的事务。
-
保持事务的颗粒度在一个合理的处理数据量的范围之内,待处理的数据量越小,则事务处理的时间越短,则出现僵持锁的几率将大幅度地降低。
-
及时提交事务,事务未提交的持续时间越长,则出现僵持锁的几率越大。
-
在一些场景中,建议使用读提交的事务隔离级别。
-
当时事务中包括很多执行语句的时候,保持每个事务的语句执行顺序都相同。
-
每个数据表都创建索引,减少索引扫描的范围、减少设置锁的数量。
-
使用串行化的事务隔离级别,也最安全的事务隔离级别,但是会大幅度降低性能,其使用方式如下所示,数据表t1执行写操作,数据表t2执行读操作:
-
创建只包含一行记录的数据表,作为全局的同步表,所有语句的执行,先从全局表中获取锁,再执行语句,达到串行化的处理。