1.背景
由于同事使用mybatisPlus根据createTime字段orderBy降序排序然后使用mybatis的分页插分页导致数据重复,创建时间相同的数据记录分页后会又可能同一条数据第一次查询显示在第一页,有可能显示在第二页,也有可能显示在第n页,这是一个随机的事件,然后我也去看了下这个问题,随便也做一个记录和分享。
2.原因
在MySQL 5.6及以后的版本上,优化器在遇到order by limit语句的时候,做了一个优化,即使用了priority queue。使用 priority queue 的目的,是在不能使用索引有序性的时候,如果要排序,并且使用了limitn,那么只需要在排序的过程中,保留n条记录即可,这样虽然不能解决所有记录都需要排序的开销,但是只需要 sort buffer少量的内存就可以完成排序。
之所以5.6出现了第二页数据重复的问题,是因为 priority queue使用了堆排序的排序方法,而堆排序是一个不稳定的排序方法,也就是相同的值可能排序出来的结果和读出来的数据顺序不一致。5.5 没有这个优化,所以也就不会出现这个问题。
快速排序和堆排序是不稳定的排序算法,也就是对于重复值是不能保证顺序的。而直接利用索引的话其返回数据是稳定的,因为索引的B+树叶子结点的顺序是唯一且一定的,快速排序适合大数据量排序,堆排序在少量排序上有优势。
根本原因还是mysql的底层的order by使用的内存排序算法使用的是快速排序和堆排序,具体使用哪一种排序需要根据数据量的大小mysql的优化器会选择相应的排序算法,恰好这两种算法是不稳定的算法,具体要深究为啥?答案就得去研究mysql的底层和这两种算法的利弊了,也没有必要,其实这种问题都可以百度得到,但是遇到这种问题记录总结下,形成自己的套路,以后就不至于为啥遇到这种奇葩的问题的时候感到蛋疼。
3.解决办法
3.1 不处理
分页只是查询数据,总的来说,数据总数都是在的,只不过是数据重复不影响查看该条数据的,这个就看你的业务对这个的容忍程度了,大不了多看一次呗。
3.2 排序时新增唯一字段保证排序的稳定性或把不唯一的排序字段替换为唯一的排序字段
在要排序的字段后面再加一个值唯一的字段来保证排序的稳定性
比如 order by create_time desc, id desc 或者 用主键id替换create_time
3.3 数据库不排序写代码排序重新分页
不加id 排序的话就写两个查询,把createTime相同的数据找出来(这个sql根据时间排序) + createTime不相同的数据找出来(不根据时间排序),用这个PageHelper来在内存中重新分页也是可以的,就让相同的createTime的数据放在最后一页,或者是把数据按条件查询的数据全部查出来(不排序查),查出来的数据是一个List<实体>,然后使用JDK的集合比较器排序
//排序(使用匿名内部类)
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s1.getSname().compareTo(s2.getSname());
}
});
或者Java8 使用 stream().sorted()对List集合进行排序
// 按age排序升序
List<Student> ageAscList = studentList.stream().sorted(Comparator.comparing(Student::getAge)).collect(Collectors.toList());
//根据age降序
List<Student> reversedList = studentList.stream().sorted(Comparator.comparing(Student::getAge).reversed()).collect(Collectors.toList());
//降序
studentList.sort((t1, t2) -> t2.getCreateTime().compareTo(t1.getCreateTime()));
//升序
studentList.sort((t1, t2) -> t1.getCreateTime().compareTo(t2.getCreateTime()));
//多字段排序
List<Student> ageHeightList = studentList.stream().sorted(Comparator.comparing(Student::getId).thenComparing(Student::getHeight)).collect(Collectors.toList());
利用JDK的对集合的排序特性或者是JDK的流式接口排序特性,先根据id降序然后根据创建时间降序这种两次多字段排序,然后在用PageHelper来分页,这种如果数据量大的话不是一个好的方案,这种方案要写的代码有点多,直接加个id一起order by改动小也方便修改,所以这个方法也不推荐,只是一个思路而已。
4.总结
软件 = 数据结构 + 算法
这句永恒的经典真理永不过时, 这两个点才是修炼的武功(内功)秘籍。