欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于游標(biāo)的分頁接口實現(xiàn)代碼示例

 更新時間:2018年11月12日 15:15:46   作者:賈順名  
這篇文章主要給大家介紹了關(guān)于基于游標(biāo)的分頁接口實現(xiàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

前言

分頁接口的實現(xiàn),在偏業(yè)務(wù)的服務(wù)端開發(fā)中應(yīng)該很常見,PC時代的各種表格,移動時代的各種feed流、timeline。

出于對流量的控制,或者用戶的體驗,大批量的數(shù)據(jù)都不會直接返回給客戶端,而是通過分頁接口,多次請求返回數(shù)據(jù)。

而最常用的分頁接口定義大概是這樣的:

router.get('/list', async ctx => {
 const { page, size } = this.query

 // ...

 ctx.body = {
 data: []
 }
})

// > curl /list?page=1&size=10

接口傳入請求的頁碼、以及每頁要請求的條數(shù),我個人猜想這可能和大家初學(xué)的時候所接觸的數(shù)據(jù)庫有關(guān)吧- -,我所認(rèn)識的人里邊,先接觸MySQL、SQL Server什么的比較多一些,以及類似的SQL語句,在查詢的時候基本上就是這樣的一個分頁條件:

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

或者類似的Redis中針對zset的操作也是類似的:

> ZRANGE <key> <start> <stop>

所以可能習(xí)慣性的就使用類似的方式創(chuàng)建分頁請求接口,讓客戶端提供page、size兩個參數(shù)。

這樣的做法并沒有什么問題,在PC的表格,移動端的列表,都能夠整整齊齊的展示數(shù)據(jù)。

但是這是一種比較常規(guī)的數(shù)據(jù)分頁處理方式,適用于沒有什么動態(tài)的過濾條件的數(shù)據(jù)。

而如果數(shù)據(jù)是實時性要求非常高的那種,存在有大量的過濾條件,或者需要和其他數(shù)據(jù)源進(jìn)行對照過濾,用這樣的處理方式看起來就會有些詭異。

頁碼+條數(shù) 的分頁接口的問題

舉個簡單的例子,我司是有直播業(yè)務(wù)的,必然也是存在有直播列表這樣的接口的。

而直播這樣的數(shù)據(jù)是非常要求時效性的,類似熱門列表、新人列表,這些數(shù)據(jù)的來源是離線計算好的數(shù)據(jù),但這樣的數(shù)據(jù)一般只會存儲用戶的標(biāo)識或者直播間的標(biāo)識,像直播間觀看人數(shù)、直播時長、人氣,這類數(shù)據(jù)必然是時效性要求很高的,不可能在離線腳本中進(jìn)行處理,所以就需要接口請求時才進(jìn)行獲取。

而且在客戶端請求的時候也是需要有一些驗證的,舉例一些簡單的條件:

  • 確保主播正在直播
  • 確保直播內(nèi)容合規(guī)
  • 檢查用戶與主播之間的拉黑關(guān)系

這些在離線腳本運(yùn)行的時候都是沒有辦法做到的,因為每時每刻都在發(fā)生變化,而且數(shù)據(jù)可能沒有存儲在同一個位置,可能列表數(shù)據(jù)來自MySQL、過濾的數(shù)據(jù)需要用Redis中來獲取、用戶信息相關(guān)的數(shù)據(jù)在XXX數(shù)據(jù)庫,所以這些操作不可能是一個連表查詢就能夠解決的,它需要在接口層來進(jìn)行,拿到多份數(shù)據(jù)進(jìn)行合成。

而此時采用上述的分頁模式,就會出現(xiàn)一個很尷尬的問題。

也許訪問接口的用戶戾氣比較重,將第一頁所有的主播全部拉黑了,這就會導(dǎo)致,實際接口返回的數(shù)據(jù)是0條,這個就很可怕了。

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

這種情況客戶端是該按照無數(shù)據(jù)來展示還是說緊接著要去請求第二頁數(shù)據(jù)呢。

所以這樣的分頁設(shè)計在某些情況下并不能夠滿足我們的需求,恰巧此時發(fā)現(xiàn)了Redis中的一個命令:scan。

游標(biāo)+條數(shù) 的分頁接口實現(xiàn)

scan命令用于迭代Redis數(shù)據(jù)庫中所有的key,但是因為數(shù)據(jù)中的key數(shù)量是不能確定的,(線上直接執(zhí)行keys會被打死的),而且key的數(shù)量在你操作的過程中也是時刻在變化的,可能有的被刪除,可能期間又有新增的。

所以,scan的命令要求傳入一個游標(biāo),第一次調(diào)用的時候傳入0即可,而scan命令的返回值則有兩項,第一項是下次迭代時候所需要的游標(biāo),而第二項是一個集合,表示本次迭代返回的所有key。

以及scan是可以添加正則表達(dá)式用來迭代某些滿足規(guī)則的key,例如所有temp_開頭的key:scan 0 temp_*,而scan并不會真的去按照你所指定的規(guī)則去匹配key然后返回給你,它并不保證一次迭代一定會返回N條數(shù)據(jù),有極大的可能一次迭代一條數(shù)據(jù)都不返回。

如果我們明確的需要XX條數(shù)據(jù),那么按照游標(biāo)多次調(diào)用就好了。

// 用一個遞歸簡單的實現(xiàn)獲取十個匹配的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

這樣的使用方式給了我一些思路,打算按照類似的方式來實現(xiàn)分頁接口。

不過將這樣的邏輯放在客戶端,會導(dǎo)致后期調(diào)整邏輯時候變得非常麻煩。需要發(fā)版才能解決,新老版本兼容也會使得后期的修改束手束腳。

所以這樣的邏輯會放在服務(wù)端來開發(fā),而客戶端只需要將接口返回的游標(biāo)cursor在下次接口請求時攜帶上即可。

大致的結(jié)構(gòu)

對于客戶端來說,這就是一個簡單的游標(biāo)存儲以及使用。

但是服務(wù)端的邏輯要稍微復(fù)雜一些:

  • 首先,我們需要有一個獲取數(shù)據(jù)的函數(shù)
  • 其次需要有一個用于數(shù)據(jù)過濾的函數(shù)
  • 有一個用于判斷數(shù)據(jù)長度并截取的函數(shù)
function getData () {
 // 獲取數(shù)據(jù)
}

function filterData () {
 // 過濾數(shù)據(jù)
}

function generatedData () {
 // 合并、生成、返回數(shù)據(jù)
}

實現(xiàn)

node.js 10.x已經(jīng)變?yōu)榱薒TS,所以示例代碼會使用10的一些新特性。

因為列表大概率的會存儲為一個集合,類似用戶標(biāo)識的集合,在Redis中是set或者zset。

如果是數(shù)據(jù)源來自Redis,我的建議是在全局緩存一份完整的列表,定時更新數(shù)據(jù),然后在接口層面通過slice來獲取本次請求所需的部分?jǐn)?shù)據(jù)。

P.S. 下方示例代碼假設(shè)list的數(shù)據(jù)中存儲的是一個唯一ID的集合,而通過這些唯一ID再從其他的數(shù)據(jù)庫獲取對應(yīng)的詳細(xì)數(shù)據(jù)。

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 |
+-----+---------+------+--------+

列表數(shù)據(jù)在全局緩存

// 完整列表在全局的緩存
let globalList = null

async function updateGlobalData () {
 globalList = await redis.smembers('list')
}

updateGlobalData()
setInterval(updateGlobalData, 2000) // 2s 更新一次

獲取數(shù)據(jù) 過濾數(shù)據(jù)函數(shù)的實現(xiàn)

因為上邊的scan示例采用的是遞歸的方式來進(jìn)行的,但是可讀性并不是很高,所以我們可以采用生成器Generator來幫助我們實現(xiàn)這樣的需求:

// 獲取數(shù)據(jù)的函數(shù)
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 獲取對應(yīng)的用戶詳細(xì)數(shù)據(jù)
 const results = await mysql.query(`
 SELECT * FROM user_info
 WHERE uid in (${piece})
 `)

 // 過濾所需要的函數(shù),會在下方列出來
 yield filterData(results)
 } while (index++ < count)
}

同時,我們還需要有一個過濾數(shù)據(jù)的函數(shù),這些函數(shù)可能會從一些其他數(shù)據(jù)源獲取數(shù)據(jù),用來校驗列表數(shù)據(jù)的合法性,比如說,用戶A有一個黑名單,里邊有用戶B、用戶C,那么用戶A訪問接口時,就需要將B和C進(jìn)行過濾。
抑或是我們需要判斷當(dāng)前某條數(shù)據(jù)的狀態(tài),例如主播是否已經(jīng)關(guān)閉了直播間,推流狀態(tài)是否正常,這些可能會調(diào)用其他的接口來進(jìn)行驗證。

// 過濾數(shù)據(jù)的函數(shù)
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)
 ])

 // 正確的狀態(tài)
 if (isLive && !inBlackList) {
 return item
 }
 }))

 // 過濾無效數(shù)據(jù)
 return validList.filter(i => i)
}

最后拼接數(shù)據(jù)的函數(shù)

上述兩個關(guān)鍵功能的函數(shù)實現(xiàn)后,就需要有一個用來檢查、拼接數(shù)據(jù)的函數(shù)出現(xiàn)了。

用來決定何時給客戶端返回數(shù)據(jù),何時發(fā)起新的獲取數(shù)據(jù)的請求:

async function generatedData ({
 cursor,
 size,
}) {
 let list = globalList

 // 如果傳入游標(biāo),從游標(biāo)處截取列表
 if (cursor) {
 // + 1 的作用在下邊有提到
 list = list.slice(list.indexOf(cursor) + 1)
 }

 let results = []

 // 注意這里的是 for 循環(huán), 而非 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,
 // 如果還有數(shù)據(jù),那么就需要將本次
 // 我們返回列表最后一項的 ID 作為游標(biāo),這也就解釋了接口入口處的 indexOf 為什么會有一個 + 1 的操作了
 cursor: list[size - 1].id,
 }
 }
 }

 return {
 list: results,
 }
}

非常簡單的一個for循環(huán),用for循環(huán)就是為了讓接口請求的過程變?yōu)榇校诘谝淮谓涌谡埱竽玫浇Y(jié)果后,并確定數(shù)據(jù)還不夠,還需要繼續(xù)獲取數(shù)據(jù)進(jìn)行填充,這時才會發(fā)起第二次請求,避免額外的資源浪費(fèi)。

在獲取到所需的數(shù)據(jù)以后,就可以直接return了,循環(huán)終止,后續(xù)的生成器也會被銷毀。

以及將這個函數(shù)放在我們的接口中,就完成了整個流程的組裝:

router.get('/list', async ctx => {
 const { cursor, size } = this.query

 const data = await generatedData({
 cursor,
 size,
 })

 ctx.body = {
 code: 200,
 data,
 }
})

這樣的結(jié)構(gòu)返回值大概是,一個list與一個cursor,類似scan的返回值,游標(biāo)與數(shù)據(jù)。

客戶端還可以傳入可選的size來指定一次接口期望的返回條數(shù)。

不過相對于普通的page+size分頁方式,這樣的接口請求勢必會慢一些(因為普通的分頁可能一頁返回不了固定條數(shù)的數(shù)據(jù),而這個在內(nèi)部可能執(zhí)行了多次獲取數(shù)據(jù)的操作)。

不過用于一些實時性要求強(qiáng)的接口上,我個人覺得這樣的實現(xiàn)方式對用戶會更友好一些。

兩者之間的比較

這兩種方式都是很不錯的分頁方式,第一種更常見一些,而第二種也不是靈丹妙藥,只是在某些情況下可能會好一些。

第一種方式可能更多的會應(yīng)用在B端,一些工單、報表、歸檔數(shù)據(jù)之類的。

而第二種可能就是C端用會比較好一些,畢竟提供給用戶的產(chǎn)品;

在PC頁面可能是一個分頁表格,第一個展示10條,第二頁展示出來8條,但是第三頁又變成了10條,這對用戶體驗來說簡直是個災(zāi)難。

而在移動端頁面可能會相對好一些,類似無限滾動的瀑布流,但是也會出現(xiàn)用戶加載一次出現(xiàn)2條數(shù)據(jù),又加載了一次出現(xiàn)了8條數(shù)據(jù),在非首頁這樣的情況還是勉強(qiáng)可以接受的,但是如果首頁就出現(xiàn)了2條數(shù)據(jù),嘖嘖。

而用第二種,游標(biāo)cursor的方式能夠保證每次接口返回數(shù)據(jù)都是size條,如果不夠了,那就說明后邊沒有數(shù)據(jù)了。
對用戶來說體驗會更好一些。(當(dāng)然了,如果列表沒有什么過濾條件,就是一個普通的展示,那么建議使用第一種,沒有必要添加這些邏輯處理了)

小結(jié)

當(dāng)然了,這只是從服務(wù)端能夠做到的一些分頁相關(guān)的處理,但是這依然沒有解決所有的問題,類似一些更新速度較快的列表,排行榜之類的,每秒鐘的數(shù)據(jù)可能都在變化,有可能第一次請求的時候,用戶A在第十名,而第二次請求接口的時候用戶A在第十一名,那么兩次接口都會存在用戶A的記錄。

針對這樣的情況,客戶端也要做相應(yīng)的去重處理,但是這樣一去重就會導(dǎo)致數(shù)據(jù)量的減少。
這又是一個很大的話題了,不打算展開來講。。
一個簡單的欺騙用戶的方式,就是一次接口請求16條,展示10條,剩余6條存在本地下次接口拼接進(jìn)去再展示。

文中如果有什么錯誤,或者關(guān)于分頁各位有更好的實現(xiàn)方式、自己喜歡的方式,不妨交流一番。

參考資料

redis | scan

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

相關(guān)文章

  • 用Electron寫個帶界面的nodejs爬蟲的實現(xiàn)方法

    用Electron寫個帶界面的nodejs爬蟲的實現(xiàn)方法

    這篇文章主要介紹了用Electron寫個帶界面的nodejs爬蟲的實現(xiàn)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • nodejs個人博客開發(fā)第三步 載入頁面

    nodejs個人博客開發(fā)第三步 載入頁面

    這篇文章主要為大家詳細(xì)介紹了nodejs個人博客開發(fā)的載入頁面,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-04-04
  • Node.js中如何合并兩個復(fù)雜對象詳解

    Node.js中如何合并兩個復(fù)雜對象詳解

    下面這篇文章主要給大家介紹了在Node.js中如何合并兩個復(fù)雜對象的方法,文中給出了詳細(xì)的示例代碼,相信對大家的理解和學(xué)習(xí)具有一定的參考借鑒價值,有需要的朋友可以參考,下面來一起看看吧。
    2016-12-12
  • 在node中如何使用 ES6

    在node中如何使用 ES6

    這篇文章主要介紹了在node中如何使用 ES6 ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-04-04
  • Cli Todo命令行todo工具使用演示

    Cli Todo命令行todo工具使用演示

    這篇文章主要為大家介紹了Cli Todo命令行todo工具使用演示示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • 在Linux上用forever實現(xiàn)Node.js項目自啟動

    在Linux上用forever實現(xiàn)Node.js項目自啟動

    在一臺計算機(jī)上手動跑Node項目簡單,node xx.js就搞定了,想讓Node項目后臺運(yùn)行,雖然不能直接用node命令搞定,但是在安裝了forever這個包以后,還是很輕松的。不過要是在遠(yuǎn)程服務(wù)器上構(gòu)建Node項目,如果沒法自啟動,一旦服務(wù)器重啟,那就麻煩了。
    2014-07-07
  • 關(guān)于npm主版本升級及其相關(guān)知識點(diǎn)總結(jié)

    關(guān)于npm主版本升級及其相關(guān)知識點(diǎn)總結(jié)

    npm是Node.js默認(rèn)的包管理器,以javascript?編寫的軟件包管理系統(tǒng)用于分享和使用代碼,下面這篇文章主要給大家介紹了關(guān)于npm主版本升級及其相關(guān)知識點(diǎn)總結(jié)的相關(guān)資料,需要的朋友可以參考下
    2022-12-12
  • nodejs+express搭建多人聊天室步驟

    nodejs+express搭建多人聊天室步驟

    本篇文章給大家詳細(xì)講解了nodejs+express搭建一個簡易的多人聊天室的詳細(xì)步驟,有興趣的朋友學(xué)習(xí)下。
    2018-02-02
  • nodejs下打包模塊archiver詳解

    nodejs下打包模塊archiver詳解

    這篇文章主要介紹了nodejs下打包模塊archiver的使用方法,非常簡單實用,這里推薦給有需要的小伙伴。
    2014-12-12
  • node基于puppeteer模擬登錄抓取頁面的實現(xiàn)

    node基于puppeteer模擬登錄抓取頁面的實現(xiàn)

    本篇文章主要介紹了node基于puppeteer模擬登錄抓取頁面的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-05-05

最新評論