血通常的教训,请慎用insert into select。同事应用以后,致使公司损失了近10w元,最终被公司开除。java
事情的原由
公司的交易量比较大,使用的数据库是mysql,天天的增量差很少在百万左右,公司并无分库分表,因此想维持这个表的性能只能考虑作数据迁移。mysql
同事李某接到了这个任务,因而他想出了这两个方案sql
- 先经过程序查询出来,而后插入历史表,再删除原表
- 使用insert into select让数据库IO来完成全部操做
第一个方案使用的时候发现一次性所有加载,系统直接就OOM了,可是分批次作就过多io和时间长,因而选用了第二种方案,测试的时候没有任何问题,开开心心上线,而后被开除。数据库
到底发生了啥,咱们复盘一下
- 先来看第一个方案,先看伪代码
// 一、查询对应须要迁移的数据 List<Object> list = selectData(); // 二、将数据插入历史表 insertData(list); // 三、删除原表数据 deleteByIds(ids);
咱们能够从这段代码中看到,OOM的缘由很简单,咱们直接将数据所有加载内存,内存不爆才怪。网络
- 再来看看第二个方案,到底发生了啥
为了维持表的性能,同时保留有效数据,通过商量定了一个量,保留10天的数据,差很少要在表里面保留1kw的数据。因此同事就作了一个时间筛选的操做,直接insert into select ... dateTime < (Ten days ago),爽极了,直接就避免了要去分页查询数据,这样就不存在OOM啦。还简化了不少的代码操做,减小了网络问题。性能
为了测试,还特地建了1kw的数据来模拟,测试环境固然是没有问题啦,顺利经过。考虑到这个表是一个支付流水表,因而将这个任务作成定时任务,而且定在晚上8点执行。测试
晚上量也不是很大,天然是没有什么问题,可是次日公司财务上班,开始对帐,发现资金对不上,不少流水都没有入库。最终排查发现晚上8点以后,陆陆续续开始出现支付流水插入失败的问题,不少数据所以丢失。最终定位到了是迁移任务引发的问题,刚开始还不明因此,白天没有问题,而后想到晚上出现这样的状况多是晚上的任务出现了影响,最后停掉该任务的第二次上线,发现没有了这样的状况。code
复盘
- 问题在哪里?
为何停掉迁移的任务以后就行了呢?这个insert into select操做到底作了什么?咱们来看看这个语句的explain。 咱们不难从图中看出,这个查询语句直接走了全表扫描。这个时候,咱们不难猜测到一点点问题。若是全表扫描,咱们这个表这么大,是否是意味着迁移的时间会很长?倘若咱们这个迁移时间为一个小时,那是否是意味着就解释了咱们白天没有出现这样问题的缘由了。可是全表扫描是最根本的缘由吗?blog
咱们不妨试试,一边迁移,一边作些的操做,还原现场。最终仍是会出现这样的问题。这个时候,咱们能够调整一下,大胆假设,若是不全表扫描,是否是就不会出现这样的问题。当咱们将条件修改以后,果真发现没有走了全表扫描了。最终再次还原现场,问题解决了 索引
得出结论:全表扫描致使了此次事故的发生。
这样作就解决了发生的问题,可是作为陆陆续续开始失败这个就很差解释了。
- 缘由
在默认的事务隔离级别下:insert into a select b的操做a表示直接锁表,b表是逐条加锁。这也就解释了为何出现陆续的失败的缘由。在逐条加锁的时候,流水表因为多数是复合记录,因此最终部分在扫描的时候被锁定,部分拿不到锁,最终致使超时或者直接失败,还有一些在这加锁的过成功成功了。
- 为何测试没有问题?
在测试的时候充分的使用了正式环境的数据来测试,可是别忽视一个问题,那就是测试环境毕竟是测试环境,在测试的时候,数据量真实并不表明就是真实的业务场景。比方说,这个状况里面就少了一个迁移的时候,大量数据的插入这样的状况。最终致使线上bug
解决办法
既然咱们避免全表扫描就能够解决,咱们避免它就好了。想要避免全表扫描,对where后面的条件作索引,让咱们的select查询都走索引便可
insert into还能用吗?
能够
总结
使用insert into select的时候请慎重,必定要作好索引