新手问题 游标分页的实现思路

42thcoder · 发布于 2015年9月24日 · 最后由 ssybb1988 回复于 2015年9月24日 · 1077 次阅读
6764

问题

通常来说, 分页通过 offsetlimit 来实现, 借助第三方 gem 我们可以这样写: collection.page(params[:page]).per_page(params[:per_page]).padding(params[:offset]).

但是, 当列表数据会频繁变动时(eg, 论坛里热门话题的回复列表, 正在进行的游戏列表 ), 传统分页就会出现前后两页数据重复的问题, 基于游标的分页就上场啦. 基于游标的分页, 就是指明上一页最后一项的 ID, 取该项之后的 N 项.

思路

简单场景

所谓简单场景, 就是列表没有复杂的排序, 按ID 或者 created_at 来排序. 这个问题可以这样来解决:

collection.where('id > ?', params[:last_id]).limit(params[:limit])

现实场景

真实项目中, 大多数列表的排序都很复杂, 例如正在进行的游戏列表, 很可能要根据权重, 开始时间和热门程度进行组合排序, 这种情况下的解决方案就没那么美了.

虚拟列

第一种思路是利用MySQL的自增虚拟列, 伪代码是这样的:

select * from 
    (select distinct (@i:=@i+1) as i,  games.* from games , ( select   @i:=0 ) as it order by games.updated_at, games.score, games.popular ) as ordered_columns where ordered_columns.i > 
        (select ordered_columns.i from 
            (select distinct (@i:=@i+1) as i,  games.* from games , ( select   @i:=0 ) as it where games.id = 10 order by games.updated_at, games.score, games.popular ) as ordered_columns);

代码中的 @user2 @user5 都是 @i, 搞不懂这是啥情况, 呼唤 @huacnlee

这样的代码很明显太丑了, 也很难跟ArctiveRecord 配合使用, 或许可以用 Arel ? 😄

数组

第二种思路适用于数据量较少的情景. 我直接贴(简化)代码了:

ids         = collection.ids
start_index = ids.index(params[:last_id])
collection.first ? collection.first.class.find_ordered(ids[start_index, params[:limit]]) : collection

MySQL 有个小坑, select * from games where id in (7, 1, 10) 查到的数据排序, 跟你想的不太一样, 你可以试一下. 这里monkey patch 了下:

module Extensions::ActiveRecord::FindByOrderedIds
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      sanitized_id_string = ids.map {|id| connection.quote(id)}.join(",")
      where(id: ids).order("FIELD(id, #{sanitized_id_string})")
    end
  end
end

ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)

暂时只想到这两种思路, 论坛里的各位大神, 大家是如何解决类似问题的?

共收到 2 条回复
11222

我觉得你把事情弄得非常复杂,而且没有实用性。

简单的置顶,比如l类似ruby china这样的,你可以把字段直接加入查询,比如按照is_sticky, updated_at排序。

复杂的置顶,置顶项的分离和单独的缓存是必须的,不可能个个请求都过数据库。比如说你按某种热度排序,取前10个或更少,那么controller要先拿到这些缓存的置顶项,剩下的正常贴就按照普通的pagination另外拿,算分页时去除置顶项的长度就可以了。如果是第一页就在array左侧插入置顶项。很简单的。置顶项的计算方法和缓存的更新时间你自己可以随意掌握。

18930

#1楼 @billy 置顶和游标分页 的场景还是有些区别的:

  1. 置顶只需要维护少量的数据,游标分页是有可能遍历所有数据的,并且维护排序。
  2. 置顶不能保证每两次分页取到的数据不重叠,游标分页可以做到。

LZ的方案简单粗暴有效,思路二会更快到达瓶颈,毕竟把 ids 都取了出来。 等到数据量大了以后,缓存是要有的,能用 redis 的 zset 话最方便,不能的话继续分情况,读多写少的话,自己乖乖维护这个列表,如果读少写多的话。。。把提这个需求的产品经理拉过来,我们好好聊聊。。。。

btw, 微博的 feed 流也没有做到准确的游标分页

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册