说到数据库死锁,相信每个人都深受其害。歪哥前段时间也遭遇了一次,造成了线上数据库长时间阻塞。
1、innodb_deadlock_detect(死锁检测)
这个打开时,数据库会自己检测死锁情况,一旦出现死锁则自动回滚一个。当然打开的代价是会降低业务的并发性能。
2、innodb_lock_wait_timeout(锁等待时间)
当出现上面提到的死锁等情况时,达到这个最大等待时间还没有释放,才会回滚处理。这个默认是50s,当然实际可能不会设置这么长。
这次线上故障的前提是数据库关闭了死锁检索,并且等待时间为50s,这就造成了至少长达一分钟的锁表无法释放。
当然最自然的解决方案是开启死锁检测或降低等待时间,不过这种操作肯定是要谨慎再谨慎,需要大量调研论证的。一般DBA都会建议我们先从业务上入手优化。
死锁产生有四个必要条件,实际场景中出现死锁一般都是产生了环路等待,即,一条语句持有资源A,又去申请锁资源B,另一条语句持有资源B,同时去申请A。
那么问题来了,执行一条语句为什么需要这么多资源呢?只需要一条资源是不是就不会锁了?实际上当然不可能只需要一个资源,即使我们觉得走到某条索引只需要对此加锁就够了,其实可能也会涉及多个索引加锁,如果锁多条资源的顺序不确定,在高并发场景下就易出现环路。
歪哥开头提到的线上故障就是遇到了环路等待,本来两条sql应该是先后执行,更新同一条语句的不同状态,结果某些异常操作触发了并行,由于两条sql查询条件不完全一样,走到不同索引,造成对多条索引加锁顺序正好相反,产生环路。
歪哥也是借此机会恶补了不少数据库相关知识,关于环路等待产生死锁问题有不少文章分析,歪哥找到一个比较浅显易懂的分享给大家。
CREATE TABLE `t3` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` varchar(5),
`b` varchar(5),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_a` (`a`),
KEY `idx_b` (`b`)
)
update t3 set b = '' where a = "1";
update t3 set b = '' where b = "2";
分析一下加锁过程,如下
t1事务的sql会走到uk_a索引,因此先锁比较好理解,uk_a是二级索引,最终会通过主键索引更新,因此再锁主键索引,然后更新字段b,实际也会把uk_b也锁住,t2同理。
这样就出现了图中框出的环路顺序。
上面的例子提到了会对主键索引加锁,下面我们再看一个例子来对其有更清晰的认知。
对这样一个表T2,以及事务session1、session2,我们可以看到,经过二级索引过滤筛选后,检索出的记录主键都是id=1和id=6,然而他们的顺序是相反的,而update语句实际上会一条一条加锁更新,这样session1先锁主键索引id=1的记录,后锁id=6的记录,session2正好相反,这就造成了加锁顺序不一致导致的死锁。
除了开头提到的修改数据库配置参数外,从业务本身我们需要尽量避免这种场景的出现,但如果真的不能完全避免(实际这种情况才是最多的),歪哥这里有一招简单有效的方法,就是在查询条件里增加主键(物理主键,一般就是自增id),或者干脆直接用主键更新,这样走主键索引,精确锁定记录,不会涉及到后面一大堆的加锁顺序等问题。对于开头提到的线上问题,最终歪哥就是采用了这种方式,简单有效值得一试!但是,要注意一种情况,如果加的是id in ()条件,那么还是可能会出现问题,这个可能是跟上面提到的主键加锁顺序有关,但不管怎样,情况已经改善了很多。