基于游标的分页接口实现代码示例

分页接口的实现,在偏业务的服务端开发中应该很常见,PC时代的各种表格,移动时代的各种feed流、timeline。

出于对流量的控制,或者用户的体验,大批量的数据都不会直接返回给客户端,而是通过分页接口,多次请求返回数据。

而最常用的分页接口定义大概是这样的:

router.get('/list', async ctx => { const { page, size } = this.query // ... ctx.body = { data: [] } }) // > curl /list?page=1&size=10

接口传入请求的页码、以及每页要请求的条数,我个人猜想这可能和大家初学的时候所接触的数据库有关吧- -,我所认识的人里边,先接触MySQL、SQL Server什么的比较多一些,以及类似的SQL语句,在查询的时候基本上就是这样的一个分页条件:

SELECT <column> FROM <table> LIMIT <offset>, <rows>

或者类似的Redis中针对zset的操作也是类似的:

> ZRANGE <key> <start> <stop>

所以可能习惯性的就使用类似的方式创建分页请求接口,让客户端提供page、size两个参数。

这样的做法并没有什么问题,在PC的表格,移动端的列表,都能够整整齐齐的展示数据。

但是这是一种比较常规的数据分页处理方式,适用于没有什么动态的过滤条件的数据。

而如果数据是实时性要求非常高的那种,存在有大量的过滤条件,或者需要和其他数据源进行对照过滤,用这样的处理方式看起来就会有些诡异。

页码+条数 的分页接口的问题

举个简单的例子,我司是有直播业务的,必然也是存在有直播列表这样的接口的。

而直播这样的数据是非常要求时效性的,类似热门列表、新人列表,这些数据的来源是离线计算好的数据,但这样的数据一般只会存储用户的标识或者直播间的标识,像直播间观看人数、直播时长、人气,这类数据必然是时效性要求很高的,不可能在离线脚本中进行处理,所以就需要接口请求时才进行获取。

而且在客户端请求的时候也是需要有一些验证的,举例一些简单的条件:

确保主播正在直播

确保直播内容合规

检查用户与主播之间的拉黑关系

这些在离线脚本运行的时候都是没有办法做到的,因为每时每刻都在发生变化,而且数据可能没有存储在同一个位置,可能列表数据来自MySQL、过滤的数据需要用Redis中来获取、用户信息相关的数据在XXX数据库,所以这些操作不可能是一个连表查询就能够解决的,它需要在接口层来进行,拿到多份数据进行合成。

而此时采用上述的分页模式,就会出现一个很尴尬的问题。

也许访问接口的用户戾气比较重,将第一页所有的主播全部拉黑了,这就会导致,实际接口返回的数据是0条,这个就很可怕了。

let data = [] // length: 10 data = data.filter(filterBlackList) return data // length: 0

这种情况客户端是该按照无数据来展示还是说紧接着要去请求第二页数据呢。

所以这样的分页设计在某些情况下并不能够满足我们的需求,恰巧此时发现了Redis中的一个命令:scan。

游标+条数 的分页接口实现

scan命令用于迭代Redis数据库中所有的key,但是因为数据中的key数量是不能确定的,(线上直接执行keys会被打死的),而且key的数量在你操作的过程中也是时刻在变化的,可能有的被删除,可能期间又有新增的。

所以,scan的命令要求传入一个游标,第一次调用的时候传入0即可,而scan命令的返回值则有两项,第一项是下次迭代时候所需要的游标,而第二项是一个集合,表示本次迭代返回的所有key。

以及scan是可以添加正则表达式用来迭代某些满足规则的key,例如所有temp_开头的key:scan 0 temp_*,而scan并不会真的去按照你所指定的规则去匹配key然后返回给你,它并不保证一次迭代一定会返回N条数据,有极大的可能一次迭代一条数据都不返回。

如果我们明确的需要XX条数据,那么按照游标多次调用就好了。

// 用一个递归简单的实现获取十个匹配的key await function getKeys (pattern, oldCursor = 0, res = []) { const [ cursor, data ] = await redis.scan(oldCursor, pattern) res = res.concat(data) if (res.length >= 10) return res.slice(0, 10) else return getKeys(cursor, pattern, res) } await getKeys('temp_*') // length: 10

这样的使用方式给了我一些思路,打算按照类似的方式来实现分页接口。

不过将这样的逻辑放在客户端,会导致后期调整逻辑时候变得非常麻烦。需要发版才能解决,新老版本兼容也会使得后期的修改束手束脚。

所以这样的逻辑会放在服务端来开发,而客户端只需要将接口返回的游标cursor在下次接口请求时携带上即可。

大致的结构

对于客户端来说,这就是一个简单的游标存储以及使用。

但是服务端的逻辑要稍微复杂一些:

首先,我们需要有一个获取数据的函数

其次需要有一个用于数据过滤的函数

有一个用于判断数据长度并截取的函数

function getData () { // 获取数据 } function filterData () { // 过滤数据 } function generatedData () { // 合并、生成、返回数据 }

实现

node.js 10.x已经变为了LTS,所以示例代码会使用10的一些新特性。

因为列表大概率的会存储为一个集合,类似用户标识的集合,在Redis中是set或者zset。

如果是数据源来自Redis,我的建议是在全局缓存一份完整的列表,定时更新数据,然后在接口层面通过slice来获取本次请求所需的部分数据。

P.S. 下方示例代码假设list的数据中存储的是一个唯一ID的集合,而通过这些唯一ID再从其他的数据库获取对应的详细数据。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/708a57a27c9df9b3eb4b8bf65ef9a113.html