深度分页,我都是这么玩的
大家好,我是架构摆渡人。这是实践经验系列的第十一篇文章,这个系列会给大家分享很多在实际工作中有用的经验,如果有收获,还请分享给更多的朋友。
分页查询,无论是在B端的系统,还是C端的应用,都有着广泛的应用。只不过是应用方式和对性能的要求不一样而已。
在B端的系统中一般都是一个列表,下面有一个分页的组件,可以选择第几页的数据,可以进行上下分页,这种就是最常见的分页方式,对应到数据库中我们常实现的方式就是limit 0,10这种。
在C端的应用中,也有分页查询的场景,但是对应性能要求比较高,我们都知道传统limit在页数越大的时候,性能也越差,主要是跳过的数据越多,回表的次数也多,这些时间都浪费了。所以一般都不会在C端应用中使用传统的分页方式。
其次C端应用的分页都是没有分页组件的,以订单列表来说,是个分页查询的场景,在APP中是滑动下拉加载分页。
为了提高性能,一般会采用ID直接定位的方式来做分页,改写SQL如下:
select * from table where id < #{lastId} order by id desc limit #{limit}
改写之后,就能根据上次返回的ID直接通过聚簇索引定位,然后取出对应的条数即可。
这样改完之后,无论用户滑到多少页,性能都是很快的。但是这种方式也会存在一个问题,就是你的主键ID必须是自增有序才行。可能有同学会问:还有无序的主键ID吗?
肯定是有的,假设你们业务发展很快,需要考虑整个机房不能提供服务的场景,这个时候就需要做异地多活了。
在多活场景下,如果是单元库,会进行双向复制,此时主键ID如果都是自增的就会存在冲突问题。当然可以通过设置不同机房不同的自增步长来解决,这种方式不太灵活,当后面扩机房的时候又需要调整。
另一种方式就是接入分布式ID,分布式ID一般的解决方案有snowflake,segment等。但在分布式场景下要提供完全递增有序的很难,所以上面的分页也会存在一定的问题。比如订单列表,用户下完单后去列表查看,很有可能最新的订单不在第一条,因为你的ID不是全局递增。
这样的问题我们如何解决呢?大家想想,在现实生活中什么是递增的呢?答案就是时间。
所以,我们可以在表中单独加个时间字段来保证有序性。这个时间的精度一定要高,比如微妙,纳秒级别,这样的高精度才能防止重复。还得建一个唯一索引来保证唯一性,确保万无一失。
有了这个时间字段,程序就不要去赋值了,直接使用数据库的默认值,当然批量插入需要注意,因为批量插入的时间会一样,所以程序中要禁止批量插入。
假设时间字段有重复的,会对分页造成影响吗?肯定有影响的,我们举个例子看下就知道了。
4 2022-01-01 12:12:12.111431
3 2022-01-01 12:12:12.111431
2 2022-01-01 12:12:10.111431
1 2022-01-01 12:12:09.111431
我们的SQL如下:
select * from table order by time desc limit 1
那么第一页的时候是没有lastTime值的,所以在拼接SQL的地方要做判断。第一页查出的数据是ID为1,时间为2022-01-01 12:12:12.111431的数据。
第二页的SQL如下:
select * from table where time < '2022-01-01 12:12:12.111431' order by time desc limit 1
获取的结果是ID为3,时间为2022-01-01 12:12:10.111431的数据,你会发现ID为2的数据丢失了,因为它的时间跟第一条一模一样,这就是问题所在,所以我们要保证时间字段的唯一性。
如果非得要通过SQL解决也是可以的,我们可以将查询的SQL改写下,如下:
select * from table where time < '2022-01-01 12:12:12.111431' or (time='2022-01-01 12:12:12.111431' and id < 4) order by time desc,id desc limit 1
通过加入or条件匹配最后一个时间,如果时间又相同的就会符合条件,并且这条数据的时间是小于之前最后一条数据的ID, 这样就可以把重复的数据查出来了。
需要注意的是之前我们返回给客户端只需要最后一条数据的ID, 那么现在就要返回ID+时间了。