一、背景说明
在线上环境中数据库监控中,突然出现Lock wait timeout exceeded; try restarting transaction 错误,并伴随着大量数据库请求超时报警。
二、了解问题
- 问题说明
update data_sku SET sku_str ='测试',modified = now() where id = 123
java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction
该问题发生在Mysql数据库中Innodb 引擎表中,当Innodb 引擎表中出现行锁等待超时并且行锁等待超时,会在等待行锁进程中抛出
Lock wait timeout exceeded; try restarting transaction错误。
- 数据库行锁配置
在mysql数据库中,innodb_lock_wait_timeout 该参数控制 Innodb 行锁等待的超时时间,可通过以下命令查询数据库配置的当前值。
SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
- 问题场景
1) 多台服务器同一时间请求数据库中同一条data_sku数据
2) 同一个事务对同一条data_sku数据进行插入和修改操作
3) 并发对一条data_sku进行插入或修改操作
4) 在查询或修改语句中导致了锁表
三、排查问题
1) 借助mysql数据库工具排查问题
--正在执行的慢SQL记录线程
show processlist
--当前运行的所有事务
mysql> SELECT * FROM information_schema.INNODB_TRX;
--当前出现的锁
mysql> SELECT * FROM information_schema.INNODB_LOCKs;
--锁等待的对应关系
mysql> SELECT * FROM information_schema.INNODB_LOCK_waits;
注:由于数据库中以上数据一切正常,未能实践中使用到
2) 使用排除法排查问题,根据报错sql语句,在项目中检索查找使用地方,最终确定如下调用代码:
List<MySku> skus = new ArrayList<>();
/**......**/
long failCount = skus.parallelStream()
.mapToLong(o -> {
try {
return skuService.create(o);
} catch (Throwable e) {
return -1L;
}
})
.filter(o -> o <= 0)
.count();
parallelStream并行执行流操作,底层借助fork/join框架提供并发执行能力,非线程安全,并行流在遍历时无法绝对有序。
关于parallelStream的介绍详见:https://blog.youkuaiyun.com/darrensty/article/details/79283146
四、定位问题
1) 结合出错时间日志,发现运营人员多次进行了导入操作,由于前端按钮未做请求控制,最终导致运营人员使用同一个导入文件执行了多次导入操作。
2) 多次导入操作命中到不同服务器,而每个服务器又都使用parallelStream进行的插入操作,最终导致问题的产生
模拟问题重现:在本地环境将mySql数据库,SET GLOBAL innodb_lock_wait_timeout=1; 将行锁等待的超时时间调低,开发工具类使用多线程调用以上skuService.create(o);方法,最终在测试环境重现问题,详细代码如下所示:
private void saves(List<MySku> skus){
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
long failCount = skus.parallelStream()
.mapToLong(o -> {
try {
return skuService.create(o);
} catch (Throwable e) {
return -1L;
}
})
.filter(o -> o <= 0)
.count();
System.out.println(failCount);
}
}).start();
}
}
五、解决问题
1) 使用stream流式串行调用skuService.create(o)方法
2) 前端对导入按钮进行控制