redis> SMEMBER list > 1 > 2 > 3 mysql> SELECT * FROM user_info +-----+---------+------+--------+ | uid | name | age | gender | +-----+---------+------+--------+ | 1 | Niko | 18 | 1 | | 2 | Bellic | 20 | 2 | | 3 | Jarvis | 22 | 2 | +-----+---------+------+--------+
列表数据在全局缓存
// 完整列表在全局的缓存 let globalList = null async function updateGlobalData () { globalList = await redis.smembers('list') } updateGlobalData() setInterval(updateGlobalData, 2000) // 2s 更新一次
获取数据 过滤数据函数的实现
因为上边的scan示例采用的是递归的方式来进行的,但是可读性并不是很高,所以我们可以采用生成器Generator来帮助我们实现这样的需求:
// 获取数据的函数 async function * getData (list, size) { const count = Math.ceil(list.length / size) let index = 0 do { const start = index * size const end = start + size const piece = list.slice(start, end) // 查询 MySQL 获取对应的用户详细数据 const results = await mysql.query(` SELECT * FROM user_info WHERE uid in (${piece}) `) // 过滤所需要的函数,会在下方列出来 yield filterData(results) } while (index++ < count) }
同时,我们还需要有一个过滤数据的函数,这些函数可能会从一些其他数据源获取数据,用来校验列表数据的合法性,比如说,用户A有一个黑名单,里边有用户B、用户C,那么用户A访问接口时,就需要将B和C进行过滤。
抑或是我们需要判断当前某条数据的状态,例如主播是否已经关闭了直播间,推流状态是否正常,这些可能会调用其他的接口来进行验证。
// 过滤数据的函数 async function filterData (list) { const validList = await Promise.all(list.map(async item => { const [ isLive, inBlackList ] = await Promise.all([ http.request(`https://XXX.com/live?target=${item.id}`), redis.sismember(`XXX:black:list`, item.id) ]) // 正确的状态 if (isLive && !inBlackList) { return item } })) // 过滤无效数据 return validList.filter(i => i) }
最后拼接数据的函数
上述两个关键功能的函数实现后,就需要有一个用来检查、拼接数据的函数出现了。
用来决定何时给客户端返回数据,何时发起新的获取数据的请求:
async function generatedData ({ cursor, size, }) { let list = globalList // 如果传入游标,从游标处截取列表 if (cursor) { // + 1 的作用在下边有提到 list = list.slice(list.indexOf(cursor) + 1) } let results = [] // 注意这里的是 for 循环, 而非 map、forEach 之类的 for await (const res of getData(list, size)) { results = results.concat(res) if (results.length >= size) { const list = results.slice(0, size) return { list, // 如果还有数据,那么就需要将本次 // 我们返回列表最后一项的 ID 作为游标,这也就解释了接口入口处的 indexOf 为什么会有一个 + 1 的操作了 cursor: list[size - 1].id, } } } return { list: results, } }
非常简单的一个for循环,用for循环就是为了让接口请求的过程变为串行,在第一次接口请求拿到结果后,并确定数据还不够,还需要继续获取数据进行填充,这时才会发起第二次请求,避免额外的资源浪费。
在获取到所需的数据以后,就可以直接return了,循环终止,后续的生成器也会被销毁。
以及将这个函数放在我们的接口中,就完成了整个流程的组装:
router.get('/list', async ctx => { const { cursor, size } = this.query const data = await generatedData({ cursor, size, }) ctx.body = { code: 200, data, } })
这样的结构返回值大概是,一个list与一个cursor,类似scan的返回值,游标与数据。
客户端还可以传入可选的size来指定一次接口期望的返回条数。