Redis有序集合命令ZREVRANGEBYSCORE詳解與應用

大大的微笑發表於2016-10-17

Redis有序集合命令ZREVRANGEBYSCORE詳解與應用

本文是我在redis中文網翻譯團隊翻譯redis命令的相關內容,也是取得翻譯團隊同意後在CSDN同步發表
redis.cn翻譯團隊 也歡迎有興趣和能力的朋友加入!

根據分數排序獲取成員列表 ZREVRANGEBYSCORE

1 簡介

ZREVRANGEBYSCORE 返回有序集合中指定分數區間內的成員,分數由高到低排序。

2 語法

2.1 完整示例

ZREVRANGEBYSCORE key max min WITHSCORES LIMIT offset count

2.2 說明

指令 是否必須 說明
ZREVRANGEBYSCORE 指令
key 有序集合鍵名稱
max 最大分數值,可使用”+inf”代替
min 最小分數值,可使用”-inf”代替
WITHSCORES 將成員分數一併返回
LIMIT 返回結果是否分頁,指令中包含LIMIT後offset、count必須輸入
offset 返回結果起始位置
count 返回結果數量

提示:

  • "max""min"引數前可以加 "(" 符號作為開頭表示小於, "(" 符號與成員之間不能有空格
  • 可以使用 "+inf""-inf" 表示得分最大值和最小值
  • "max""min" 不能反, "max" 放後面 "min"放前面會導致返回結果為空
  • 計算成員之間的成員數量不加 "(" 符號時,引數 "min""max" 的位置也計算在內。
  • ZREVRANGEBYSCORE集合中按得分從高到底排序,所以"max"在前面,"min"在後面, ZRANGEBYSCORE集合中按得分從底到高排序,所以"min"在前面,"max"在後面。

3 返回值

指定分數範圍的元素列表。

4 示例

4.1 按分數倒序返回成員

"+inf" 或者 "-inf" 來表示記錄中最大值和最小值。
"(" 左括號來表示小於某個值。目前只支援小於操作的 "(" 左括號, 右括號(大於)目前還不能支援。

redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREVRANGEBYSCORE myzset +inf -inf
1) "three"
2) "two"
3) "one"
redis> ZREVRANGEBYSCORE myzset 2 1
1) "two"
2) "one"
redis> ZREVRANGEBYSCORE myzset 2 (1
1) "two"
redis> ZREVRANGEBYSCORE myzset (2 (1
(empty list or set)
redis> 

4.2 按分數倒序返回成員以及分數

ZREVRANGEBYSCORE 指令中, 還可以使用WITHSCORES 關鍵字來要求返回成員列表以及分數。

redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREVRANGEBYSCORE myzset +inf -inf WITHSCORES
1) "three"
2) "3"
3) "two"
4) "2"
5) "three"
6) "1"
redis> ZREVRANGEBYSCORE myzset 2 1 WITHSCORES
1) "two"
2) "2"
3) "one"
4) "1"
redis> ZREVRANGEBYSCORE myzset 2 (1
1) "two"
redis> ZREVRANGEBYSCORE myzset (2 (1
(empty list or set)
redis> 

4.3 分頁返回資料

可以通過 LIMIT 對滿足條件的成員列表進行分頁。一般會配合 "+inf" 或者 "-inf" 來表示最大值和最小值。
下面的例子中就是使用分頁引數返回資料的例子。

redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREVRANGEBYSCORE myzset +inf (2 WITHSCORES LIMIT 0 1 
1) "three"
2) "3"
redis> ZREVRANGEBYSCORE myzset +inf (2 WITHSCORES LIMIT 2 3
(empty list or set)
redis> 

5 應用

5.1 時事新聞

我們的網易新聞、騰訊新聞、一點資訊、虎嗅、快科技等新聞類應用場景中, 新聞會根據時間排列,形成一個佇列,
在這些網站瀏覽新聞時, 每次重新整理頁面都能獲取到最新的資料,當然還可以通過分頁檢視歷史資料,
但每次重新整理首頁都將是一個以最新資料為基礎進行分頁。
這又區別於面前今日頭條的下拉重新整理機制,關於今日頭條的下拉重新整理機制我們下節講,這一節先講講如何實現新聞資訊佇列分頁。

場景及需求:

根據資料型別分頻道來展示資訊,比如時事新聞,根據時間排序,使用者進入網站就獲取到最新時事新聞,後臺能實時向時事新聞頻道增加新聞,且只要使用者重新整理就能看到最新的時事新聞,然後以最新新聞為基礎進行分頁展示。
我們根據時間新增5條新聞到news頻道。

這裡寫圖片描述

redis> zadd news 201610022301 '{"title" : "new1", "time": 201610022301}'
(integer) 1
redis> zadd news 201610022302 '{"title" : "new2", "time": 201610022302}'
(integer) 1
redis> zadd news 201610022303 '{"title" : "new3", "time": 201610022303}'
(integer) 1
redis> zadd news 201610022304 '{"title" : "new4", "time": 201610022304}'
(integer) 1
redis> zadd news 201610022305 '{"title" : "new5", "time": 201610022305}'

實際場景中new1、news2…等等都應該是一個json物件,包含新聞標題、時間、作者、型別、圖片URL…等等資訊。
然後當使用者請求新聞時,我們會使用這個命令查詢出最新幾條記錄:

redis> ZREVRANGEBYSCORE news +inf -inf LIMIT 0 2
1) "{\"title\" : \"new5\", \"time\": 201610022305}"
2) "{\"title\" : \"new4\", \"time\": 201610022304}"

上面的例子中,我們從news頻道查詢出來了2條最新記錄。如果使用者翻頁,我們會使用最新記錄中時間最大的記錄做為引數進行分頁。依次查詢第二頁,第三頁。

redis> ZREVRANGEBYSCORE news 201610022305 -inf LIMIT 2 2
1) "{\"title\" : \"new3\", \"time\": 201610022303}"
2) "{\"title\" : \"new2\", \"time\": 201610022302}"
redis> ZREVRANGEBYSCORE news 201610022305 -inf LIMIT 4 2
1) "{\"title\" : \"new1\", \"time\": 201610022301}"
redis> 

總結: 我這裡沒有使用當前時間作為最大值查詢,是為了避免使用者電腦時間不準確導致請求失敗。比如剛裝系統,或者BOIS電池沒有電,不能儲存系統當前時間,就會導致系統時間都是1970年1月1日,這樣,去查詢,是查詢不到資料的,比如之前的太平洋電腦網,如果你將電腦系統時間改成1970年1月1日,再去訪問,就無法獲取新資料了。現在有不少網站有類似問題.
在這個業務場景中,如果某個編輯新發布一條新聞到時事新聞中,原來在翻頁檢視新聞的使用者是不受影響的,只有使用者重新整理瀏覽器或者重新整理首頁,就會看到最新的資料了。

5.2 時事新聞下拉更新

上面時事新聞的例子能解決一部分業務場景的需求,但是如果遇到比較較真的產品經理,需要做成類似今日頭條下拉重新整理最新資料,上拉獲取歷史記錄的功能,那麼一個 ZREVRANGEBYSCORE 命令肯定是解決不了問題的。下面我講講我一些解決方案的思路。
首先,從佇列的角度看需求,以201610022303這個時間為界限,下拉重新整理如果獲最新資料,就是這樣:

redis> ZREVRANGEBYSCORE news +inf 201610022303 LIMIT 0 2
1) "{\"title\" : \"new5\", \"time\": 201610022305}"
2) "{\"title\" : \"new4\", \"time\": 201610022304}"

然後,上拉重新整理獲取歷史資料,分頁查詢,都沒有問題:

redis> ZREVRANGEBYSCORE news 201610022303 -inf LIMIT 0 2
1) "{\"title\" : \"new3\", \"time\": 201610022303}"
2) "{\"title\" : \"new2\", \"time\": 201610022302}"
redis> ZREVRANGEBYSCORE news 201610022303 -inf LIMIT 2 2
1) "{\"title\" : \"new1\", \"time\": 201610022301}"
redis> 

不過,你會發現,下拉重新整理時,資料是從最大時間到最小時間排序,如果編輯新增一條資料,就會打亂我們佇列的順序

redis> zadd news 201610022306 '{"title" : "new6", "time": 201610022306}'
(integer) 1
redis> ZREVRANGEBYSCORE news +inf 201610022303 LIMIT 2 2
1) "{\"title\" : \"new4\", \"time\": 201610022304}"
2) "{\"title\" : \"new3\", \"time\": 201610022303}"
redis> 

前一秒,查詢第一頁資料是news5和news4,我翻頁的時候,結果變成了news4和news3,news4重複了。
雖然不會對上拉重新整理查詢歷史資料有影響,但是作為一個實時性非常強的新聞應用,使用者更關注的應該是最新的新聞的內容,也就是下拉重新整理的功能,如果一個基本的排版都重複,使用者的耐心恐怕不會給你更多機會。
想到這,感覺太可怕了!產品經理要是知道,一定會噴死我們的。當然,這不是最要命的問題,最要命的問題是如果使用者要是回到第一頁(非重新整理),就會發現,當初的第一頁已經不是剛剛看過的第一頁了。
第一頁多了一條news6的新聞!

redis> ZREVRANGEBYSCORE news +inf 201610022303 LIMIT 0 2
1) "{\"title\" : \"new6\", \"time\": 201610022306}"
2) "{\"title\" : \"new5\", \"time\": 201610022305}"

還有一個問題,就是如果使用者前一天看到第二頁,隔天登入,然後請求最新新聞,還會是第二頁嗎?
可以考慮以下解決思路:
首先,需要前端和後端配合,前端定義三個動作,首次登入,下拉重新整理(獲取最新資料),上拉重新整理(獲取歷史資料),定義一個下拉重新整理時間和一個上拉重新整理時間,
如果是首次登入,先判斷下拉重新整理時間是否比今天凌晨時間小,如果小,證明已經隔天了,將下拉重新整理時間設定成當天凌晨時間;
如果下拉重新整理時間比今天凌晨時間大,說明使用者看過今天新聞,就直接用下拉重新整理時間請求新資料。
對於上拉重新整理請求資料,可以在判斷首次請求時,出現隔天后,將上拉重新整理時間置為當天凌晨時間。這樣在請求歷史資料時,不至於丟失中間的資料。
當然,關鍵還是命令的使用,查詢歷史資料時,還可以使用 ZREVRANGEBYSCORE 來獲取資料,不過,獲取最新資料,就可以使用 ZRANGEBYSCORE 來取資料。
圖片1

redis> zadd news 201610030110 '{"title" : "new7", "time": 201610030110}'
(integer) 1
redis> zadd news 201610030111 '{"title" : "new8", "time": 201610030111}'
(integer) 1
redis> zadd news 201610030112 '{"title" : "new9", "time": 201610030112}'
(integer) 1
redis> ZRANGEBYSCORE news 201610030000 +inf LIMIT 0 2
1) "{\"title\" : \"new7\", \"time\": 201610030110}"
2) "{\"title\" : \"new8\", \"time\": 201610030111}"
redis> ZRANGEBYSCORE news 201610030000 +inf LIMIT 2 2
1) "{\"title\" : \"new9\", \"time\": 201610030112}"

總結: 時事新聞下拉更新這個例子中, 可以使用每次請求資料的最大時間戳和最小時間戳來作為下一次請求的輸入,這裡的時間戳的概念不限於時間,可以是資料庫裡的自增ID,然後在快取中定義一個當天的ID,每當隔天出現,就取獲取這個ID,然後通過這個ID作為當天凌晨的時間戳標記,
獲取比這個ID大或者小的結果集來完成獲取最新資料和歷史資料的功能,這樣就可以解決客戶端時間不準確後獲取不到資料的問題。

相關文章