• 起因背景

本次开发是在一个旧功能的增加新的逻辑,其中有这么一个消息重发的模块。在正常的Kafka消息推送过程中,因为异常、数据条件暂时无法确定是否能够下发的情况下,把这条消息暂时存到一张Mysql表中,并使用定时任务每隔一段时间扫一下这张表,把消息重新尝试下发Kafka。如果发送失败,则把这条消息的重试次数加1。如果发送成功,则把这条消息的从重试表中删除。

  • 问题现象

因为这是一个上线了几年的功能了,且这次开发的需求是在下发的时候进行的调整,所以一开始对这块并没有太多在意。直到昨天测试同学给我讲了他发现的一个现象,执行一次重试任务之后,有些数据的重试次数加了不止1次,但是有些数据的重试次数却没有增加。那么疑点就来了,根据测试提供的充实任务入口名称,我找到了这段已经在生产环境跑了好几年的代码,初步看了一遍基本定位到问题所在。

基本逻辑是这样的

  1. 根据分页参数分页从Mysql捞取待重试数据,需要查询已重试次数小于一个设定值的,比如5次
  2. 将待重试数据调用推送Kafka方法尝试进行推送,这之中包括一系列验证逻辑
  3. 推送成功的数据从表中清除
  4. 推送失败的数据在表中更新重试次数加1
  • 问题原因

那么根据这样的逻辑,以及问题表现出来的现象,其实就基本初步可以有个判定了。我们举个例子来说明下,首先表里有如下这么多不同重试次数的数据。且假定当前分页参数每页查询条数为4条,最大重试次数为3次

那么第一次分页查询是过滤掉其中大于等于3次的数据,且取其中前4条并更新重试次数加1,如下

这个循环结束,开始进入下一个循环,照旧根据根据查询条件过滤掉大于等于3次的数据,并取第二页的4条数据,即往右偏移4条数据进行处理,对重试次数进行加1处理,其中有一条数据达到了3条的上限

此时已经能够看出问题所在了,上一个循环中,有两条数据已经被更新为了重试次数3次,那么再来在这次的循环中,还使用重试次数小于3次的条件查询的话,那两条数据就会被过滤掉了,由后面的未满3次的数据往前移动填上空缺位置。在这样的情况下,仍旧按照原来的分页结果取第二页数据的话,那么后面的数据往前移动填补了空缺的最前面两条此时就会被忽略掉了。

继续循环,按照第一开始的数量,应该还有一次可遍历结果。

最终,因为上次遍历又有一条数据到达了3条的上限,最后最后一个循环的时候当前分页内不到结果了。

这便是漏数据原因之一,为什么要说之一呢?因为当我根据这个情况把原来的代码逻辑调整了下,把更新语句都挪到了原来的翻页查询之外,重试次数异常增加的情况还是有发生的。

不过我在原来代码的遍历中发现了这么一段

看起来,当初写这个代码的人在开发的时候就已经发现了,明明按照查询数量进行计算分页了,为什么还会出现分页查询结果为空的情况,所以才加上了这么一句。但是我不知道他为什么已经发现这种不正常的情况,却没有好好检查代码,而是潦草的加了个判空退出的逻辑就结束了。

  • 另一个原因

再次回到我们的问题,用最直接的办法,重新跑下代码发现每次分页捞取到的数据是有重复的部分的,那么就可以肯定分页查询的SQL这边也是有问题的。于是找到SQL语句,在客户端里执行调试下看下。

select ID, CREATE_TIME
FROM TABLE_NAME
WHERE RETRY < 6
ORDER BY CREATE_TIME DESC
limit 0, 5
select ID, CREATE_TIME
FROM TABLE_NAME
WHERE RETRY < 6
ORDER BY CREATE_TIME DESC
limit 5, 5

从上面的SQL语句以及对应结果可以看到,ID为1065306和1065304的的两条数据在两次查询中都出现了,这样的查询结果就导致重试次数异常的增加了。同时因为总查询页数没有变,那么一次执行下来,就必然会有漏掉的数据,这也是漏数据的另一个原因了。

  • 处理办法

发生上面这个查询重复的原因

在MySQL 5.6的版本上,优化器在遇到order by limit语句的时候,做了一个优化,即使用了priority queue。

使用priority queue目的
就是在不能使用索引有序性的时候,若要排序,并且使用了limit n,那么只需要在排序的过程中,保留n条记录即可。这样虽然不能解决所有记录都需要排序的开销,但是只需要sort buffer 少量的内存就可以完成排序。

因为 priority queue 使用了堆排序的排序方法,而堆排序是一个不稳定的排序方法,也就是相同的值可能排序出来的结果和读出来的数据顺序不一致。MySQL 5.5版本没有优化,所以不会产生此问题

解决方案1,给排序使用的字段增加一个索引,根据索引排序,利用索引的有序性来解决 priority queue的堆排序不稳定的问题

解决方案2,在排序条件中新增一个字段,当第一个排序字段的值重复的时候再按照第二个字段来排序,推荐的使用自增主键的ID来实现,自增ID天然无重复,免去自己另外定义的字段仍然可能有重复的问题。

  • 小结
  1. 在分页查询遍历中,不要更新分页查询使用到的条件字段
  2. Mysql的Order By条件在遇到重复值的时候,多次查询出来的排序结果可能是混乱的(根据Mysql版本情况所有不同),请添加必要的索引或者使用无重复值的字段进行排序