业务背景

事情的起因是这样的…几个月前做过一个统计类型的job,上线之后小修小补了几次一直运行的很平稳,就是有一个缺点:慢。起初我一直以为是因为数据量过大导致的,每天早上六点准时开跑,一般要到下午一两点才能跑完,其实现在想想这么长时间的运行肯定是不合理的,而且本身业务的数据量也没有大到那个地步,但是由于一直工作太忙了(懒),再加上本身不算特别重要的模块就没有过多在意,但是由于我们的job只有一个节点,所以如果存在这种长时间运行的job就会导致我们项目的发版时间非常受限制,基本每天只有下午三点到六点左右可以发版,终于我在忍无可忍之后决定重构代码,彻底优化。

优化的结果还是十分理想的,现在只需要到七点半就可以跑完了,但是我却发现了一个严重的问题,快是快了…可是跑完了表里没数据啊…

发现过程

1.第一反应,不用说,肯定觉得是我代码改的有问题,虽然测试环境试跑过,但是毕竟线上和测试有差异,于是我熟练的登陆服务器查看log,搜索关键字,结果sb了…完全没有任何报错;
2.那既然代码不报错,只能认为是数据库的问题,最简单粗暴的办法,把log里的sql拿出来测试环境执行一下,结果又sb了…测试环境完美插入数据;
3.此时再次开始怀疑人生,查了查前两天的数据,更发现了一个诡异的问题,我的优化是周一发版的,但是从周六开始就已经没有数据了…是不是可以甩锅了…我没动啊,不是我啊,我不知道啊;
4.事情绝对没有那么简单,还是往下查吧,此时我干了一件本来不应该但是也没办法的事情,直接在线上数据库执行insert语句,好在只是统计数据,希望其他同学轻易不要动线上数据。但是这一下问题有眉目了,一样的sql测试环境虽然没问题,线上确实插入0条!!确实是数据库的问题!!仔细一看报错,我了个擦,主键冲突?????
5.再次开始怀疑人生…大家写insert语句肯定也不会写主键值吧,主键不都是auto increment的吗…怎么会冲突呢?我把报冲突的主键值拿出来一查,我去还真有…难道说我竟然无意间触发了MySQL的插入bug???正在我第n次怀疑人生的时候我突然明白问题的所在了,主键值是2147483647,这是什么这么眼熟?这是tmd整型的最大值啊!!!我擦,原来数据库主键被我用光了…牛逼啊…

解决过程

1.我此时第一反应是冷静,虽然诡异但是肯定是有原因的,首先我们公司的业务肯定远远没有达到这个量级,况且这张统计表是按月分表的,不可能积攒这么多数据,而且实际查看后发现只有130w数据而已;
2.数据库的自增id按理说有bug的概率很小,那么看来应该还是有问题,此时唯一的办法就是钻回代码一探究竟,好在代码我比较熟悉,操作这张表其实只有一句sql,insert into table … on duplicate key update,那看来和这个on duplicate key update逃脱不了干系了;
3.on duplicate key update是指根据表的唯一索引判断当前数据是插入还是更新,是一个很方便的语法,否则我们还需要代码实现先查询判断是否存在,再更新或者插入,不仅代码臃肿,而且多了一次IO。方便是方便,但是坑也在这里,因为这句sql不能确定是insert还是update,所以其实每次都会申请出足够的主键id用于插入,由于我的代码是批量插入,也就是说假如我一次放入1000条数据,那么主键id会先申请1000个,因为有可能全是insert不存在update,所以导致不管有没有真的插入主键id都自增了,这样id既不连续,又增长的快…这下明白了,看来问题就出现在这里,快速增长的id到达了int最大值导致后续数据全部插入失败;
4.到这里故事没有结束,因为其实我心里还是大概有量级的,21亿啊,一个月就100w+的统计量,这得重复多少次才能够21亿啊,总感觉虽然找到问题了,但是好像还不能完全说通,况且从业务上来讲,应该根本没有多少这种冲突,每次基本都是insert才对,看来事情还是没有这么简单,于是我又钻入了代码中…终于…发现了最sb的事情,我的批处理是用一个map做的,循环map得到数据然后批量插入,但是这个map是一个方法内的局部变量,但是定义在了大循环的最外层,而循环体内没有任何清除操作…也就是说map是一直在累加的,而由于业务需求,代码的循环次数是非常多的,所以说第n次总会无效插入前n-1次的累计数据…这么一算的话量就有点儿可怕…所以也最终导致了主键被用完的恐怖情况

反思

1.MySQL语法功能实现掌握不足,导致定位问题存在偏差,且编码时也没注意;
2.map的问题太低级了!在循环里用容器或者map一定要注意清空!