Mysql的故障排查并没有你想象的那么容易...
问题首先出现
一天下午,系统突然醒来,抛出异常:
仔细一看,像是事务回滚。 。异常, write 问题是由于不断回归,变成了停止问题。由于我对Mysql的锁还是比较了解的,所以开始解决这个问题。
首先,在数据库中找到Innodb Status。有关错误的最新信息将记录在 Innodb Status 中。输入如下命令:
SHOW ENGINE INNODB STATUS
死锁信息如下,简单处理一下sql信息:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-02-22 15:10:56 0x7eec2f468700
*** (1) TRANSACTION:
TRANSACTION 2660206487, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 31261312, OS thread handle 139554322093824, query id 11624975750 10.23.134.92 erp_crm__6f73 updating
/*id:3637ba36*/UPDATE tenant_config SET
open_card_point = 0
where tenant_id = 123
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1322 page no 534 n bits 960 index uidx_tenant of table `erp_crm_member_plan`.`tenant_config` trx id 2660206487 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 2660206486, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 31261311, OS thread handle 139552870532864, query id 11624975758 10.23.134.92 erp_crm__6f73 updating
/*id:3637ba36*/UPDATE tenant_config SET
open_card_point = 0
where tenant_id = 123
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 1322 page no 534 n bits 960 index uidx_tenant of table `erp_crm_member_plan`.`tenant_config` trx id 2660206486 lock mode S
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1322 page no 534 n bits 960 index uidx_tenant of table `erp_crm_member_plan`.`tenant_config` trx id 2660206486 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (1)
------------
给大家简单分析一下,解释一下这个死锁日志。事务1执行Update语句时,需要获取uidx_tenant索引,然后获取其所在位置的X锁(数字锁)。事务 2 执行相同的 Update 语句,并且还考虑 uidx_tenant。为了获得X锁(行锁),发生了break,回滚了事务1,当时我就一头雾水,我记得发生死锁的必要条件是:
- 彼此接近。
- 应用并保留条件。
- 无提款条件。
- 等一下。从日志中可以看出,事务 1 和事务 2 都在竞争同一行锁。和之前周期性的抢锁有些不同。无论怎么看,它们都不能满足循环期望条件。经过同事提醒,由于死锁软件是不可审计的,所以只能在业务代码和公司日志中审计问题。这段代码的逻辑如下:
public int saveTenantConfig(PoiContext poiContext, TenantConfigDO tenantConfig) {
try {
return tenantConfigMapper.saveTenantConfig(poiContext.getTenantId(), poiContext.getPoiId(), tenantConfig);
} catch (DuplicateKeyException e) {
LOGGER.warn("[saveTenantConfig] 主键冲突,更新该记录。context:{}, config:{}", poiContext, tenantConfig);
return tenantConfigMapper.updateTenantConfig(poiContext.getTenantId(), tenantConfig);
}
}
这段代码的意思是保存一个配置文件。如果发生独特冲突,将会更新。当然,这里写的可能不是很最新。其实你也可以用
insert into ...
on duplicate key update
。结果是一样的,但是即使用了也会有影响。同事看完代码后,给我发了当时的公司日志。
可以看到有3条日志同时发生,说明发生了唯一引用冲突,插入了更新的语句,然后就出现了break。现在答案似乎更加清晰了。
现在我们看一下我们的表结构如下(简化):
CREATE TABLE `tenant_config` (
`id` bigint(21) NOT NULL AUTO_INCREMENT,
`tenant_id` int(11) NOT NULL,
`open_card_point` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
tenant_id 用作唯一索引,我们的条件和更新都是基于唯一索引的。 。
UPDATE tenant_config SET
open_card_point = 0
where tenant_id = 123
此时我意识到插入时关闭单指针与此有关。让我们仔细看看下一步。
深度分析
我们上面提到,有3个事务进入update语句。为了简化说明,我们只需要两个事务同时进入update语句即可。下表显示了事件的顺序:
时间线 | 事务 1 | 事务 2 | 事务 3 | |
---|---|---|---|---|
1 | 插入 xx插入 | |||
2 | 获取行 X 锁定 | |||
3 | 需要知道单指针是否冲突才能获取S锁,阻止 | 需要知道单指针是否冲突才能获取S锁,阻止 | ||
4 | 提交; | 获取S锁 | 获取S锁 | |
5 | 发现唯一索引冲突,运行Update语句(此时S锁未释放) | 发现唯一索引更新,执行获取X行锁,阻塞S事务锁 3 | 获取X行锁,阻塞事务 2 S块阻塞 | |
7 | 检测到死锁,事务回滚 | 更新成功,提交; |
提示:S锁是共享锁,X锁是互斥锁。一般来说,X锁、S锁和X锁是不同的,但S锁和S锁不是互斥的。
从上面的步骤我们可以看出,这次中断的关键是获取S锁,为什么重装的时候需要获取S锁呢?因为我们需要找到唯一索引?在隔离级别RR以下,如果要读的是当前读,所以确实需要加S锁。这里可以看到唯一键已经存在了。此时,更新的执行会被两个事务中的S锁阻塞,造成等待循环的情况。
提示:在MVCC中,当前读和快照读的区别:当前读每次获取到最后一个数据时都需要加锁(可以使用共享锁或者互斥锁),如果镜像读取到这个镜像时有一笔交易。启动是通过取消日志来实现的。
这就是整个停电的原因。另一种可能发生此类死锁的情况是同时存在三个输入操作时。如果第一个插入的事务最终被回滚,那么另外两个事务也会发生。
解决方案
这里主要问题是解除S锁,这里提供三种解决方案供参考:
- 将RR隔离级别降低到RC隔离级别。这里,RC隔离级别将使用快照读,因此不会添加S锁。
- 重新输入时,使用Select *更新输入的X锁,避免进入S锁。
- 可以添加预分布式锁,可以使用Redis,或者ZK等。关于共享锁,可以参考这篇文章。我们来谈谈分割锁
第一种方法不太现实,但是隔离级别不能轻易改变。第三种方法比较复杂。所以我们最终选择了第二种方法。
总结
说了这么多,最后来做个小总结吧。在排查死锁等问题时,有时仅仅查看死锁日志可能无法解决问题。您需要查看整个公司日志、代码和表结构才能得到准确的答案。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。