分页拉取数据重复的几种解决思路

5,812 阅读10分钟

1. 常见的分页方式

1.1 页码分页

条码分页.gif

页码分页展示区域是固定的,通过调整页码,来变更展示区域的内容,第n页与第n-1页是替代(replace)关系,可查看的数据只有1页。

1.2 滑动分页

滑动分页.gif

滑动分页展示区域不是固定的,通过上页滑动,不断追加新的内容,第n页与第n-1页是追加(append)关系,滑动的越多,可查看的数据就越多。

1.3 比较

分页方式优点缺点使用场景交互方式
页码分页用户能够明确知道总页数,清晰明了,实现简单每页都需要去重新发起请求,即使该页已经访问过web端比较常用的一种分页方式翻页加载页面;首页尾页导航;
滑动分页体验比较好,加载过数据后,查看之前的数据,不需要再次加载,纵享丝滑需要缓存前面页的数据,数据量较多时,占用存储空间。移动端交互比较常用的一种分页方式下滑加载数据;上拉重载页面;

2. 数据重复问题分析

2.1 问题简述

电商系统中,用户查询已收货包裹数据时,向上滑动,新加载的第二页数据与第一页数据有重复内容。

2.2 查询sql分析

包裹表(quotation_package_info)

简要表结构如下:

字段类型简述
idint主键,自增id
package_novarchar(32)包裹单号
recycler_idint商家id
statusint包裹状态,1待收货,2已收货
create_dtdatetime创建时间
update_dtdatetime最后修改时间
express_novarchar(32)快递单号
deliver_dtdatetime发货时间
receive_dtdatetime收货时间

查询sql如下:

-- 查询某商家的所有已收货包裹,根据id倒序排列
SELECT p.id,
       p.package_no,
       p.recycler_id,
       p.express_no,
       p.status,
       p.deliver_dt,
       p.receive_dt,
       p.create_by,
       p.create_dt,
       p.update_by,
       p.update_dt
FROM quotation_package_info p
where p.recycler_id = '商家id'
  and p.status = '2'
order by p.id desc limit startIndex:pageSize;

2.3 问题分析与抽取

收货包裹分页数据异常.png

  1. 已收货包裹默认按照数据表中主键进行降序排序。
  2. 第一页的第一条数据为当前该商户已收货包裹,id最大的记录。
  3. 前端拉取到第一页数据后,该商户此时有新收货记录变动,且该条记录的id大于第一页第一条数据对应的id。
  4. 拉取第二页数据时,之前第一页的最后一条记录,被排序到第二页的第一条,出现数据重复。

与上述场景类似的分页查询都会有这种问题,抽取该问题的共性条件如下:

  1. 数据集根据某一个或几个字段进行排序。
  2. 根据排序条件无法确定一个稳定的数据集,新插入的数据,按照排序字段进行排序,可能插在整个数据集的头部或者中间。
  3. 查询某一页数据时,如果该页之前的数据集存在新插入的数据,该页一定会查询到重复的数据。

思考:该问题与分页展示方式无关,不管是“滑动分页”还是”页码分页“都存在此问题,但是”页码分页“第二页会替换掉第一页的展示内容,不易被用户察觉,而”滑动分页“的数据是追加展示的,用户易察觉到数据重复的问题。

3. 解决思路

3.1 思路一:查询完整的数据集

  • 思路简述 不进行分页,一次拉取所有的数据,原始数据集无重复,就不会拉取到重复的数据。

  • 优点

  1. 规避掉了分页存在的问题。
  2. 数据量比较少的时候,比分页查询的效率更高。
  • 缺点
  1. 数据规模比较大时,查询慢、传输慢、网络超时。
  2. 数据集较多时,前端渲染慢,用户等待加载数据时间长,易感知。
  • 适用场景 数据规模比较小的情况。

3.2 思路二:固定数据集范围

3.2.1 固定分段数据集

  • 思路简述
  1. 通过增加数据开始条件、结束条件,取一段固定的数据集。
  2. 或者调整排序条件,固定一侧数据不会变更。
  • 优点 前端不需要进行处理,数据集是固定的,只关注分页页码、每页展示条数即可

  • 缺点 适用场景有限,需要结合业务场景进行分析,必须能够固定住数据集两侧,或者至少能固定住数据集开头

  • 适用场景 能够固定住数据集的场景

3.2.1.1 案例分析

阿里云日志服务数据查询

阿里云日志查询.png

  • 第一页数据
入参:
from: 1612257085
to: 1612257985
Page: 1
Size: 20

出参:
{
    "message": "successful",
    "data": {
        "count": 20,
        "logs": [
            {},{},{}
        ]
    },
    "code": "200",
    "success": true
}
  • 第二页数据
入参:
from: 1612257085
to: 1612257985
Page: 2
Size: 20

出参:
{
    "message": "successful",
    "data": {
        "count": 20,
        "logs": [
            {},{},{}
        ]
    },
    "code": "200",
    "success": true
}
  • 分析
  1. 日志跟时间关联的,新增数据的时间戳更大,采用日志生成时间戳倒序排列的话,如果不限制范围,分页数据会很快被覆盖掉。
  2. 上述接口通过传参 from、to 限制住了要查询的日志的开始时间戳、结束时间戳,固定了数据集的范围,解决了分页的问题。

3.2.2 固定某一页的数据集

  • 思路简述
  1. 第一页数据是固定的,前端保存第一页最后一条数据对应的排序条件的值
  2. 查询后面第n页时,将第n-1页的最后一条记录对应的排序条件的值作为过滤参数
  • 优点
  1. 限制了每一页数据的开始条件,后台服务、前端界面代码修改都比较简单
  2. 如果排序条件为主键或者索引值,查询效率会更高。
  • 缺点
  1. 只适用于单条件排序,且排序条件具有唯一性。
  2. 只适用于滑动分页,不支持页码分页,不根据页码定位数据。
  3. 未下拉刷新之前,数据集中新插入数据,无法展示。
  • 适用场景
  1. 只适用于滑动分页,单条件排序,排序条件的值在数据集中唯一。
  2. 能够容忍未下拉刷新前,数据集不展示新插入的数据。
3.2.2.1 案例分析

淘宝收藏夹 淘宝收藏夹

  • 第一页数据
入参:
{
    "startRow": 0,
    "startTime": 0,
    "pageSize": 30,
    "pageNum": 0,
    "hasMore": "true",
}
出参:
{
    "api":"mtop.taobao.mercury.platform.collections.get",
    "data":{
        "favList":[
            {},{},{},
            {
                "collectTime":"2020-03-27 21:48:03",
                "shopUrl":"//shop.m.taobao.com/shop/shopIndex.htm?seller_id=92688455",
            }
        ],
        "pageInfo":{
            "hasMore":"true",
            "nextStartTime":"1585316883404",
            "pageSize":"30",
            "preloadPage":"true",
            "startRow":"0",
            "totalCount":"0"
        }
    },
    "ret":[
        "SUCCESS::调用成功"
    ],
    "v":"5.1"
}
  • 第二页数据 手机端拉取第一页数据后,pc端再进行一条收藏,第二页无重复数据
入参:
{
    
    "startRow":0,
    "startTime":"1585316883404",
    "pageSize":30,
    "pageNum":1,
    "hasMore":"true",
}

出参:
{
    "api":"mtop.taobao.mercury.platform.collections.get",
    "data":{
        "favList":[
            {
              
                "collectTime":"2019-09-13 09:15:03",
                "shopName":"佐卡曼旗舰店",
                "shopUrl":"//shop.m.taobao.com/shop/shopIndex.htm?seller_id=2973681982"
            }
        ],
        "pageInfo":{
            "hasMore":"true",
            "nextStartTime":"1568337303676",
            "pageSize":"30",
            "preloadPage":"false",
            "startRow":"0",
            "totalCount":"0"
        }
    },
    "ret":[
        "SUCCESS::调用成功"
    ],
    "v":"5.1"
}

  • 分析
  1. 淘宝收藏夹是存在数据集不固定的情况的,目前是按照收藏顺序倒序排列的,如果此时手机端拉取了第一页的收藏记录,此时pc端新加入收藏,新收藏的商品会被插入到数据集的最前面。
  2. 淘宝分页查询收藏夹接口,第2页会传入第一页最后一条数据的收藏时间,第3页会传入第2页最后一条数据的收藏时间,保证拉取到的数据不会重复。
  3. 可以倒推出查询收藏夹接口是按照收藏时间倒序排列,且通过某种机制保证了收藏时间不会重复(服务器时间无问题的话,精度到毫秒级,收藏时间完全重复的概率本身就比较低)。

3.3 思路三:前端过滤重复数据集

  • 思路简述
  1. 每拉取到一页数据,前端将所有的唯一id缓存
  2. 比对当前页给出的数据,如果已经存在,不进行渲染
  3. 判断当前页过滤掉的条数,比对阈值,如果超过了阈值,再拉取一页,避免用户感知到。
  • 优点 接口无需考虑具体的业务场景,直接进行分类即可,适用场景较多

  • 缺点

  1. 前端需要缓存已拉取到的所有的数据,用于判断去重,占用内存
  2. 缓存数据较多时,去重判断的次数也递增,渲染效率较低
  3. 未下拉刷新之前,数据集中新插入数据,无法展示。
  • 适用场景
  1. 一般用于滑动分页
  2. 数据量较少(缓存小,速度快),或能够推断出调用分页次数较少的场景。
  3. 能够容忍未下拉刷新前,数据集不展示新插入的数据。

3.3.1 案例分析

京东收藏夹 京东收藏夹

  • 第一页数据
入参:
cp=1
pageSize=10

出参:
{
    "cp": "1",
    "data": [
        {},{},{},{},{},
        {
            "commCategory": "670;677;680",
            "commColor": "「D42666频」",
            "commId": "8391337",
            "commSize": "单条【8G】",
            "commStatus": "1",
            "commTitle": "金士顿(Kingston)8GBDDR42666笔记本内存条骇客神条Impact系列",
            "favPeopleNum": "0",
            "favPrice": "469.000000",
            "favTime": "1547304069000",
            "venderId": "1000000192"
        }
    ],
    "errMsg": "",
    "iRet": "0",
    "totalNum": "79",
    "totalPage": "0"
}

  • 第二页数据 手机端拉取第一页数据后,pc端再进行一条收藏,第二页有重复数据
入参:
cp=2
pageSize=10

出参:
{
    "cp": "2",
    "data": [
          {
            "commCategory": "670;677;680",
            "commColor": "「D42666频」",
            "commId": "8391337",
            "commSize": "单条【8G】",
            "commStatus": "1",
            "commTitle": "金士顿(Kingston)8GBDDR42666笔记本内存条骇客神条Impact系列",
            "favPeopleNum": "0",
            "favPrice": "469.000000",
            "favTime": "1547304069000",
            "venderId": "1000000192"
        },{},{},{},{}
    ],
    "errMsg": "",
    "iRet": "0",
    "totalNum": "80",
    "totalPage": "0"
}

  • 分析
  1. 京东收藏夹是存在数据集不固定的情况的,目前是按照收藏顺序倒序排列的,如果此时手机端拉取了第一页的收藏记录,此时pc端新加入收藏,新收藏的商品会被插入到数据集的最前面
  2. 接口入参中,只传递了页码、每页条数信息
  3. 手机端拉取到第一页数据后,pc端新收藏一个商品,此时手机端拉取第二页数据,可以看出第二页的第一条与第一页的最后一条是重复的,且 totalNum+1。
  4. 接口数据有重复,客户端展示的数据是正常的,客户端进行了去重展示
  5. 可以反推出,京东收藏夹是按照收藏时间倒序排列的,且客户端进行了去重处理。

3.4 已收货包裹分页数据重复解决思路分析

  1. 收货包裹中的数据量较大,需要分页,思路一不可行
  2. 当前根据id排序,属于单条件排序,且id具有唯一性,思路二可行
SELECT p.id,
       p.package_no,
       p.recycler_id,
       p.express_no,
       p.status,
       p.deliver_dt,
       p.receive_dt,
       p.create_by,
       p.create_dt,
       p.update_by,
       p.update_dt
FROM quotation_package_info p
where p.recycler_id = '商家id'
  and p.status = '2'
  and p.id < :lastPageLastDataId
order by p.id desc limit :pageSize;
  1. 商家的已收货包裹,数据量不会特别大,客户端缓存数据量不多,而且一般用户滑动收货列表n次后,看不到记录,会采用精准搜索,思路三可行

综上,该问题可采用思路二或者思路三解决。

4. 总结

分页数据重复解决思路.png

分页数据重复问题,还是要结合具体业务场景进行分析,没有适用于所有场景的解决思路。
采用滑动分页方式的接口,数据重复是比较容易被用户感知到的,滑动分页是移动端普遍采用的一种分页方式,因此在设计移动端接口,以及在修改对应的sql语句时,要结合业务场景来进行具体分析,规避这种问题。

附:抓取移动端数据包

抓包工具:chrome debug 、wireshark、charles、fildder chrome debug: 自适应调整,抓取移动端数据包 pc端微信:微信小程序抓取数据包 charles抓包教程:juejin.cn/post/684490… https抓包原理:juejin.cn/post/684490…