InnerJoin分页导致的数据重复问题排查
2016年8月9号美好的七夕的早上,我精神抖擞地来到公司。一会之后,客服宅宅MM微信我,说一个VIP大店铺订单导出报表中一个订单有重复行。于是,我赶紧开始查探问题所在。经过一天的反复仔细追查(当然还包括各种事项的打断),终于发现这个问题的原因所在。。。
有个订单主表 o,以及一个订单商品表 i ; o 与 i 是一对多的关系:其中一个订单 d_no 会对应多个商品 t_id,而一个商品 t_id 仅对应一个订单 d_no. 那么问题在哪里呢? 有经验的同学可能已经猜到是什么原因了,不过且让我们一步步来看:
首先,肯定是复现问题。很幸运,在重复执行后,确实如商家所言,这个订单的相应商品行都重复了一次。那么,怎么排查呢? 显然要在应用程序的不同地方里打印这个订单的信息,看看究竟是从哪里开始重复的,通过一步步向源头追踪,发现从获取订单号的最源头的地方就有重复了。我开始以为是这个订单本身有特殊的地方,因此让商家只导出这个订单,然而没有重复;这说明很可能是这个订单与多个订单一起导出才导致的问题。我又怀疑是不是这个订单的重复行有某个细节地方是不一样的才重复,然而做对比后发现完全一样;
现在似乎有点扑朔迷离了。于是,我觉得应该同时打印相应的SQL以及出现该订单的信息。在重复运行多次、打印相关信息并仔细观察之后,发现这个订单在以下两个查询中分别出现了一次; 在第一个SQL结果的第一个,以及第二个SQL结果的最后一个。边界出现问题了。
select o.d_no from o, i where o.d_no = i.d_no and `o`.`dianpu_id` = ? and `o`.`xiadan_time` >= ? and `o`.`xiadan_time` <= ? and `i`.`shangpin_id` = ? order by i.d_no desc limit 1400,50; select o.d_no from o, i where o.d_no = i.d_no and `o`.`dianpu_id` = ? and `o`.`xiadan_time` >= ? and `o`.`xiadan_time` <= ? and `i`.`shangpin_id` = ? order by i.d_no desc limit 1350,50;
我终于意识到: 很可能是 Join 分页导致的问题。是不是代码分页有微妙的 Bug ? 看了下代码,没看出问题;疑问又来了: 为什么单单这个订单会重复,其它的不重复呢? 为什么搜索页面不重复,而导出报表会重复呢?按理来说,这个订单号没有特殊之处,那么别的订单号完全也可能重复;并且搜索与导出共用的相同的接口逻辑;重复抽查了几个 limit X, 50, 没发现重复的其他订单号; 于是,注意力还是转回到这两个SQL 。
究竟暗藏什么玄机? 我甚至怀疑与主键 id 值的缺漏有关,打印出 id 值后看不出什么特别的规律,很快否定了这个想法。 于是我直接在DBA界面系统打印出这两个SQL的结果,看着重复的d_no排序输出在界面上,突然灵光一闪,意识到了。。。
我执行了以下SQL,直接打印出这个查询条件下的所有 d_no,查看重复订单号的位置。
select o.d_no from o, i where o.d_no = i.d_no and `o`.`dianpu_id` = ? and `o`.`xiadan_time` >= ? and `o`.`xiadan_time` <= ? and `i`.`shangpin_id` = ? order by i.d_no;
你明白了么? 这个订单号对应两个商品,因此查出来这个订单号会出现两次,而这两次正好出现在两次查询的边界处。在 1399 的偏移量时,查出来一次,在 1400 的偏移量时,查出来一次; 因此,这个订单号会查出两次,而根据这个订单号获取其他信息后也会导出两次,订单导出是每个 limit X, 50 都会增量地导出到报表里,从而出现了最终报表中的重复行(在搜索接口中会有去重后返回给前端)。 其他的订单号之所以没有出现问题,是因为在临界处 50*X,50*X-1 处的订单号正好只对应一个商品,只会出现一次,因此避免了重复行。话说回来,其实导出报表订单重复行的概率还是比较高的,只要在 50*X, 50*X-1 出现的订单号是有对应多个商品的,就很可能在最终报表中有重复行。这个还是要看运气滴~~
问题原因定位了,解决就简单了。将 select o.d_no 改成 select distinct(o.d_no) 即可;当然,对应的 count 也要改成 count(distinct(o.d_no)) .
排查问题经验总结:
1. 排查问题的重点是找到:位置和原因;
2. 日志非常重要,在关键路径和关键状态上打关键信息日志,好过十打的代码分析和肯定应该Maybe。关键路径会帮助你排除不出错的地方,缩小问题范围,关键状态会减少需要做出的猜测,更肯定地排查问题; 关键信息或关键词能让人很快锁定问题出现的代码位置。
3. 根据经验分析可能的原因,然后采取技术手段去找到位置和原因。 排查的手段通常是: 复现问题,重复执行,静态分析,猜测原因,断点运行;
4. 问题对象是否有特殊性?针对对象本身单独进行探查;
5. 边界处一定要重视,常常是问题产生的多发地;
6. 先到此为止,睡觉!
ps: 偶滴七夕啊。。。虽然没有美女作伴,也不能这么开玩笑啊。。。