V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
bigbyto
V2EX  ›  程序员

如何处理 api 分页导致的数据重复或丢失

  •  
  •   bigbyto ·
    xingty · 2016-12-20 10:26:11 +08:00 · 11322 次点击
    这是一个创建于 2676 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我们的 api 目前是这样设计的

    /users/?page=2&pre_page=20
    

    客户端提交页数和每页的数量,服务端返回如下

    {
       "code": 0,
       "pagination": {
          "page": 1,
          "limit": 20,
          "total": 100
       }
       
       "data": {}
    }
    
    

    不过这样会出现数据重复或丢失。比如当前用户正在 app 翻页刷新,如果正好在后台删除了一条消息,那么就会因为数据变化导致分页时有一条数据丢失了。

    后来想到一个解决方案,通过 cursor 分页

    /users/?cursor=2015-01-01 15:20:30&limit=10
    

    上面就可以解决 app 端翻页时数据出现变化的情况,不过依然会有 2 个严重的问题。

    1. 时间戳重复会导致永久丢失某条数据
    2. 如果存在筛选条件(如饿了么按照经纬度排序获取数据),此方法失效

    对于第一个问题,我们想到使用主键作为过滤条件(主键递增),可以解决重复的问题。但第二个问题似乎想不出什么好的办法。

    不知道大家怎么处理分页问题,有好的建议或方案还望不吝赐教。

    38 条回复    2016-12-22 12:06:56 +08:00
    dou4cc
        1
    dou4cc  
       2016-12-20 10:36:38 +08:00
    不要搞太复杂
    把页面做成自动更新的,使后台对数据的操作实时反映在页面上
    stamaimer
        2
    stamaimer  
       2016-12-20 10:37:47 +08:00 via iPhone
    你可以看看 twitter 咋做的
    BOYPT
        3
    BOYPT  
       2016-12-20 10:39:50 +08:00   ❤️ 1
    学习 twitter 的 api 设计,使用 sinceid , lastid ; 推算 page 难维护难想难写容易错。
    q397064399
        4
    q397064399  
       2016-12-20 10:40:20 +08:00
    这个不应该是后端的事情么?
    脏读 可以通过事务控制来 防止
    learnshare
        5
    learnshare  
       2016-12-20 10:40:35 +08:00   ❤️ 1
    大部分场景下不需要考虑客户端状态同步

    如果真的需要,可以把删除动作同步到客户端;然后分页使用 itemId 作为判断依据,而不是 page + size
    xiaoyangsa
        6
    xiaoyangsa  
       2016-12-20 10:43:10 +08:00
    @learnshare 说得对,客户端不要搞太复杂。用 itemid 来分页吧。
    Ge4Los
        7
    Ge4Los  
       2016-12-20 10:51:17 +08:00
    用游标加长度来做参数。这种类似的 feed 流的接口,内容总在更新,客户端接口就适合游标了。
    loading
        8
    loading  
       2016-12-20 10:59:32 +08:00 via Android
    传入数据库的 id 字段
    bigbyto
        9
    bigbyto  
    OP
       2016-12-20 11:02:00 +08:00
    @BOYPT 谢谢,我去查一下 twitter 的分页方案。
    bigbyto
        10
    bigbyto  
    OP
       2016-12-20 11:03:15 +08:00
    @learnshare
    @Ge4Los
    @loading

    使用游标或 id 一旦出现筛选条件就会失效了,有没有一种通用的方案呢。
    murmur
        11
    murmur  
       2016-12-20 11:04:39 +08:00
    丢了就丢了呗,你看新浪微博刷新一次直接时间起飞到上个世纪,一样股票大涨
    chairuosen
        12
    chairuosen  
       2016-12-20 11:08:11 +08:00
    丢就丢了+1
    ty89
        13
    ty89  
       2016-12-20 11:10:45 +08:00
    按 lastid 来分页,只适合按照时间排序类似微博这种,万一后来来个需求让你按照热度、点赞数、评论数来生序降序就呵呵了
    bigbyto
        14
    bigbyto  
    OP
       2016-12-20 11:13:38 +08:00
    @ty89 对啊,我担心的问题就在这里。按照 id 分页,一旦将来增加排序需求的话完了。
    quericy
        15
    quericy  
       2016-12-20 11:29:51 +08:00
    我们就是动态排序的,按照 Redis 里 SortSet 得分从高到低
    当时想到个分页方案是以得分作为游标,这样每次翻页的时候获得得分更低的指定条数数据.
    而顶贴的时候得分会更新得更高,不会出现重复的问题,
    但是可能会漏掉几条翻页过程中被顶上去的帖子.

    不过这个方案没通过,最后还是用了传统分页+客户端对近几页的帖子 ID 去重
    ofblyt
        16
    ofblyt  
       2016-12-20 11:35:42 +08:00
    个人觉得只能是按照 id 排序来进行分页,包括美团在内的公司对于多维度排序的处理也只是将主要纬度的数据分别建表,按各个纬度的 id 进行排序分页
    Miy4mori
        17
    Miy4mori  
       2016-12-20 11:58:10 +08:00 via Android
    这有什么影响吗?不要做的太复杂。
    morethansean
        18
    morethansean  
       2016-12-20 12:00:42 +08:00 via Android
    为什么不按照时间戳呢
    dotudeth
        19
    dotudeth  
       2016-12-20 12:00:55 +08:00
    @learnshare 如果这个列表还掺杂着排序的操作呢(比如在后台设了权重值)。
    sorra
        20
    sorra  
       2016-12-20 12:09:42 +08:00
    办法是有,数据库与缓存做 diff 比较,但是颇为复杂。
    写文一篇 http://www.qingjingjie.com/blogs/24
    learnshare
        21
    learnshare  
       2016-12-20 12:58:29 +08:00
    @dotudeth
    @bigbyto 排序之后必须要拿第一页, itemId 是被忽略的
    814084764
        22
    814084764  
       2016-12-20 13:41:04 +08:00
    分页,连网易的微博都做不好,你还想做好?太天真了~ [手动滑稽]
    Magic347
        23
    Magic347  
       2016-12-20 14:00:34 +08:00
    数据库读写不分离一下吗?
    NeinChn
        24
    NeinChn  
       2016-12-20 14:05:29 +08:00
    的确没有太好的办法吧...
    一般都是用 SELECT * FROM table WHERE sort_field > last_id ORDER by sort_field LIMIT size.
    要么就前端处理重复,后端每次多返回一些数据...
    alouha
        25
    alouha  
       2016-12-20 15:03:40 +08:00
    看 xx app 的分页那叫一个烂,之前遇到过你说的问题,当时偷懒,让客户端对重复数据进行处理 /捂脸
    JasperYanky
        26
    JasperYanky  
       2016-12-20 15:17:09 +08:00
    题外话 强烈建议 下一页不要客户端拼参数,服务端的接口中直接指定 next url
    zvving
        27
    zvving  
       2016-12-20 16:57:15 +08:00
    一般都是按毫秒级的时间戳来分页的,别想太复杂
    kamal
        28
    kamal  
       2016-12-20 17:48:26 +08:00
    你看看豆瓣的评论怎么分页的
    zclzhangcl
        29
    zclzhangcl  
       2016-12-20 17:52:35 +08:00
    晚上刷百度贴吧时,发现贴吧的分页做的很差,经常连续很多都是前面已经出现过的。
    这个问题不是那么好解决,或者说没那么重要
    bigbyto
        30
    bigbyto  
    OP
       2016-12-20 18:24:29 +08:00
    @zclzhangcl 目前看来还是无法用简单的方法去处理分页问题。不过像 feed 流这种业务场景,倒是可以用 twitter 的 since_id 和 max_id 来处理,因为 feed 数据本身就是有序的(发布时间)。不过一旦出现其他排序条件,普通的方法还是得跪。
    bigbyto
        31
    bigbyto  
    OP
       2016-12-20 18:25:59 +08:00
    @JasperYanky 为什么不要客户端拼参数呢? 这一版的 api 我们目前还没正式使用,目前打算服务端返回一个"next"字段取代"page",当然还是由客户端这边提交过去。
    whow
        32
    whow  
       2016-12-20 20:25:59 +08:00
    取排序后分页最后一项,将参与排序的字段拼接成一个 next ,客户端下次请求的时候把 next 作为参数回传到服务端,服务端通过这个字段计算出下一页。
    ```
    ?next=&size=
    ```
    ```
    {
    list:[],
    next:""
    }
    ```
    mrliusg
        33
    mrliusg  
       2016-12-20 20:50:40 +08:00
    逻辑删除,然后返回给客户端的时候不显示出来就好了吧?
    或者直接在服务端删除,请求 20 条数据,只返回 19 条就好啦
    jyf
        34
    jyf  
       2016-12-20 21:17:03 +08:00
    多少无关紧要 请求 20 条 返回 22 条和 17 条都没什么大的关系 逻辑上没有问题就好了

    对于用户过滤这种可以考虑第一页的时候就把结果的主键给放 redis 的 sortedset 里 后面如果新增可以在缓存期内无视 而删除则无非是多了一个而已
    yidinghe
        35
    yidinghe  
       2016-12-20 21:22:18 +08:00 via Android
    分页逻辑天然就有这个问题,最好的方式是接续查询,用上一页最后一条记录作为查询下一页的条件,这样查出来既不会多也不会少。
    JasperYanky
        36
    JasperYanky  
       2016-12-20 23:19:38 +08:00
    @bigbyto 我倾向 所有需要分页的 API 外层都有一个 next 字段,里面是下次页的 url ;至于为什么不要客户端拼,很简单,你要是这个版本按照 id 分页 下个版本按照时间分页, API 能分分钟改掉并且完全不影响客户端~
    owt5008137
        37
    owt5008137  
       2016-12-21 12:05:58 +08:00 via Android
    你就不能给每条消息分配一个 ID ?然后拉到的消息和已有的 ID 相同就 update ,否则才 insert ?
    zclzhangcl
        38
    zclzhangcl  
       2016-12-22 12:06:56 +08:00
    @mrliusg 你这种处理方式会有问题,返回的总数与实际数量不符。对于一些严格的场景,这样是不行的。
    如果需要严格的话,还是按 @yidinghe 的方式来。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   960 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 20:45 · PVG 04:45 · LAX 13:45 · JFK 16:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.