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

42thcoder · September 24, 2015 · Last by ssybb1988 replied at September 24, 2015 · 6170 hits

问题

通常来说,分页通过 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)

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

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

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

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

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

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

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

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

You need to Sign in before reply, if you don't have an account, please Sign up first.