线上数据库死锁了!震惊

1,191 阅读6分钟

前段时间接到一个需求,线上有用户说数据入不了正式库,导入失败,让我排查排查;小意思啦

一、定位问题

报错信息: Lock wait timeout exceeded; try restarting transaction

扔给我一段报错信息,我英语很差啊,看不懂,翻译一下咯,我就看到两个字,死锁;还是数据库死锁,纳尼!数据库死锁这玩意对于一个成熟的项目很难碰到吧,千年等一回啊,按着别人说的步骤我操作了一下,没问题啊,我想大概和业务逻辑有关系吧!

1.1 代码流程问题

果然,大哥实现数据的导入方式是先校验,完成后将数据导入临时表,临时表如果存在校验失败信息的数据那么导入失败,界面提示失败的数据,如果没问题那么将数据导入正式库,导入完成删除临时表;

猛一看没啥问题哈,我之前文章说了我们的项目代码管理的一团乱麻,所以当我产生几次错误的数据以后,我去临时表一看,纳尼,为啥错误提示了后不把错误数据删除了呢,留着干撒;此时我就已看到胜利曙光了,接着我想产生几次正常流程的数据,额,不行,总是报错,但是也不报报错信息,what,有没有搞错,我啥也没干啊,硬着头皮debug,很完美,这个项目的模板再次有问题,无奈,我自己手工造了一个,这简直惨绝人寰,头疼;模板造好,试一条数据,没问题;接着我又看到胜利曙光了;

让别人查下生产表的临时数据,小小机器居然放了十万条数据,由于是临时表,也没加任何索引,所以我考虑起了数据多不多的问题;十万数据多吗,十万数据没索引不多吗,的确,十万数据即使没索引也的确算不上多的数据,哈哈哈哈,那它为啥死锁,我迷茫了起来,再跑, 好家伙,我本地上两万条后就开始死锁了;稍等,我要开始装了;

cgi-bin_mmwebwx-bin_webwxgetmsgimg_&MsgID=4752917848341311731&skey=@crypt_2ba84b38_5dcb8c9598086658996fa794c48087d5&mmweb_appid=wx_webfilehelper.jpg

临时表两万条数据,然后我开了六七个窗口导入,每个窗口6000条数据,我们再从头到尾梳理下流程,首先导入的时候会删除这个类型导入的临时表数据,就是说他没有在 导入失败后删除,而是在下一次导入的时候删除,而下一次导入由于种种条件导致会删除不彻底,这样随着次数增加数据就会越来越多,回过头来,由于机器配置分享给 mysql的并不多,而且删除条件也不是索引,导致删除的时候会全表扫起来,数据一多自然就慢,而我们的超时时间默认60S好像,在删除后如果此时我们另一个用户去 继续导入那么此时存在两个操作,一个是当前的删除后的插入操作,一个是删除操作,更刺激的来了,这玩意用jdbc居然拼接来了个一次插入,我是6000数据所以一次插入6000 ,60000那就一次插六万,大胆刁民,瞎胡闹,客户也不惯着,1w的插,使劲,最后搞不过,召唤我啦;

我能怎么办,我也没办法啊,我主打就是不重构,其实不能怪我,首先没人熟悉业务,我不能乱动,其次就那么一两天,还要改这改那时间不够,再然后改好了倒好, 改不好万一一个没考虑到出问题了还得担责,所以缝缝补补啦,用些漂亮布;。

1.2 数据库配置问题

上面的问题我们可以看到数据库配置的超时时间很短,其实对于批量操作,咱们超时时间尽量长点好点,太短了可能真的处理不过来,所以我们得改下超时时间;

二、怎么解决?

首先要看到两个点,代码有问题,超时时间也的确很短,有人可能会问没索引啊,这不算问题?是的,这不算问题,首先数据量不多, 其次我是真不知道哪些字段当索引能优化,没有有效字段;

针对超时时间短,我们先给他延长,发现延长了,部分场景有效,但测试过程中如果插入的过程中存在删除并且临时表有几万条数据的话那么删除会耗费很长时间导致插入迟迟插不进去

--查看
SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
--修改
SET GLOBAL innodb_lock_wait_timeout = 90;

现实中我唯唯诺诺,给它超时加上个30S;

插入还是报错,所以这个只能治标,我得对代码动手了

b8b2188a06c0438497e27b393f86ab97~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.awebp

删除那块我肯定不动,还是删除语句直接删,插入那块我把他拆开,本来拆500一次批量插入可是偶尔还是会锁,真的是很离谱呢;这都留不住,接着我把数据换成300一次批量插入,好了,再怎么跑也不会出问题了;

原来的代码大概意思是这样的:

List<Object[]> dbList = new ArrayList<Object[]>();
		for (List<String> sub : list) {
			dbList.add(sub.toArray());
		}
		int index = dbList.get(0).length;
		StringBuffer fields = new StringBuffer();
		StringBuffer signs = new StringBuffer();// 符号标记
		for (int i = 0; i < index; i++) {
			fields.append(",field" + (i + 1));
			signs.append("?,");
		}
		StringBuffer SQL = new StringBuffer("INSERT INTO temp_excel(fields) VALUES(
					signs)");
		DBUtil.executeBatch(SQL.toString(), dbList);

我简单改了下

AtomicInteger count = new AtomicInteger(0);
		int index = list.get(0).toArray().length;
		StringBuffer fields = new StringBuffer();
		StringBuffer signs = new StringBuffer();
		for (int i = 0; i < index; i++) {
			fields.append(",field" + (i + 1));
			signs.append("?,");
		}
		StringBuffer SQL = new StringBuffer("INSERT INTO temp_excel(fields) VALUES(
				signs)");
		List<Object[]> dbList = new ArrayList<Object[]>();
		for (List<String> sub : list) {
			dbList.add(sub.toArray());
			//分批插入
			if(count.getAndIncrement() % 300 == 0){
				DBUtil.executeBatch(SQL.toString(), dbList);
				dbList = new ArrayList<Object[]>();
			}

		}

别问为什么用AtomicInteger,问就是用习惯了; 为什么插入删除同步执行会出问题呢,我觉得是间隙锁导致的,插入数据量减少导致获取锁时间减少,然后执行删除,由于等待时间延长删除也可以顺利执行; 不然也没地方会导致死锁了吧,求教!

后期测试的时候发现我居然把300倍数后剩余的数据忘记入库了,赶紧的做一个补偿措施,

//补偿机制
		if(!dbList.isEmpty()){
			insertRetry(SQL,dbList);
		}

最后不满300的也入库 有人可能会问,你咋不用多线程,我也想了,只能说没必要一共一万条数据这样,多线程实数浪费,然后, 如果校验报错我也没添加删除操作,原因是我不清楚其他业务需不需要这临时表做啥,万一我删了没数据就噶了 而且我这分批入库我不觉得比他一次性入库慢,以上就是我的最小程度修改代码解决这个问题的思路,其实后面我还做了重试,就是万一报错立马重试,三次重试都报错再抛出错误,我觉得这样万一还出现死锁,我可以释放然后重试解决;不知道各位还有没有好思路

ok!完成

四、总结

我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!