使用Redis的HSCAN命令遇到的一個問題
前提
筆者最近在做一個專案時候使用Redis
存放客戶端展示的訂單列表,列表需要進行分頁。由於筆者先前對Redis
的各種資料型別的使用場景並不是十分熟悉,於是先入為主地看到Hash
型別:
USER_ID:1
ORDER_ID:ORDER_XX: {"amount": "100","orderId":"ORDER_XX"}
ORDER_ID:ORDER_YY: {"amount": "200","orderId":"ORDER_YY"}
複製程式碼
感覺Hash
型別完全滿足需求實現的場景。然後想當然地考慮使用HSCAN
命令進行分頁,引發了後面遇到的問題。
SCAN和HSCAN命令
SCAN
命令如下:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
// 返回值如下:
// 1. cursor,數值型別,下一輪的起始遊標值,0代表遍歷結束
// 2. 遍歷的結果集合,列表
複製程式碼
SCAN
命令在Redis
2.8.0版本中新增,時間複雜度計算如下:每一輪遍歷的時間複雜度為O(1)
,所有元素遍歷完畢直到遊標cursor
返回0的時間複雜度為O(N)
,其中N
為集合內元素的數量。SCAN
是針對整個Database
內的所有KEY進行漸進式的遍歷,它不會阻塞Redis
,也就是使用SCAN
命令遍歷KEY的效能會優於KEY *
命令。對於Hash
型別有一個衍生的命令HSCAN
Hash
型別及其相關屬性(Field
)的欄位:
HSCAN key cursor [MATCH pattern] [COUNT count]
// 返回值如下:
// 1. cursor,數值型別,下一輪的起始遊標值,0代表遍歷結束
// 2. 遍歷的結果集合,是一個對映
複製程式碼
筆者當時沒有詳細查閱Redis
的官方檔案,想當然地認為Hash
型別的分頁簡單如下(假設每頁資料只有1條):
// 第一頁
HSCAN USER_ID:1 0 COUNT 1 <= 這裡認為返回的遊標值為1
// 第二頁
HSCAN USER_ID:1 1 COUNT 1 <= 這裡認為返回的遊標值為0,結束迭代
複製程式碼
實際上,執行的結果如下:
HSCAN USER_ID:1 0 COUNT 1
// 結果
0
ORDER_ID:ORDER_XX
{"amount": "100","orderId":"ORDER_XX"}
ORDER_ID:ORDER_YY
{"amount": "200","orderId":"ORDER_YY"}
複製程式碼
也就是在第一輪遍歷的時候,KEY對應的所有Field-Value
已經全量返回。筆者嘗試增加雜湊集合KEY = USER_ID:1
裡面的元素,但是資料量相對較大的時候,依然沒有達到預期的分頁效果;另一個方面,嘗試修改命令中的COUNT
值,發現無論如何修改COUNT
值都不會對遍歷的結果產生任何影響(也就是還是在第一輪迭代返回全部結果)。百思不得其解的情況下,只能仔細翻閱官方檔案尋找解決方案。在SCAN
命令的COUNT
屬性描述中找到了原因:
簡單翻譯理解一下:
SCAN
命令以及其衍生命令並不保證每一輪迭代返回的元素數量,但是可以使用COUNT
屬性憑經驗調整SCAN
命令的行為。COUNT
指定每次呼叫應該完成遍歷的元素的數量,以便於遍歷集合,本質只是一個提示值。
-
COUNT
預設值為10。 - 當遍歷的目標
Set
、Hash
、Sorted Set
或者Key
空間足夠大可以使用一個雜湊表表示並且不使用MATCH
屬性的前提下,Redis
服務端會返回COUNT
或者比COUNT
大的遍歷元素結果集合。 - 當遍歷只包含
Integer
值的Set
集合(也稱為intsets
),或者ziplists
型別編碼的Hash
或者Sorted Set
集合(說明這些集合裡面的元素佔用的空間足夠小),那麼SCAN
命令會返回集合中的所有元素,直接忽略COUNT
屬性。
注意第3點,這個就是在Hash
集合中使用HSCAN
命令COUNT
屬性失效的根本原因。Redis
配置中有兩個和Hash
型別ziplist
編碼的相關配置值:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
複製程式碼
在如下兩個條件之一滿足的時候,Hash
集合的編碼會由ziplist
會轉成dict
:
- 當
Hash
集合中的資料項(即Field-Value
對)的數目超過512的時候。 - 當
Hash
集合中插入的任意一個Field-Value
對中的Value
長度超過64。
當Hash
集合的編碼會由ziplist
會轉成dict
,Redis
為Hash
型別的記憶體空間佔用優化相當於失敗了,降級為相對消耗更多記憶體的字典型別編碼,這個時候,HSCAN
命令COUNT
屬性才會起效。
案例驗證
簡單驗證一下上一節得出的結論,寫入一個測試資料如下:
// 70個X
HSET USER_ID:2 ORDER_ID:ORDER_XXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// 70個Y
HSET USER_ID:2 ORDER_ID:ORDER_YYY YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
複製程式碼
接著開始測試一下HSCAN
命令:
// 檢視編碼
object encoding USER_ID:2
// 編碼結果
hashtable
// 第一輪迭代
HSCAN USER_ID:2 0 COUNT 1
// 第一輪迭代返回結果
2
ORDER_ID:ORDER_YYY
YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
// 第二輪迭代
HSCAN USER_ID:2 2 COUNT 1
0
ORDER_ID:ORDER_XXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
複製程式碼
測試案例中故意讓兩個值的長度為70,大於64,也就是讓Hash
集合轉變為dict(hashtable)
型別,使得COUNT
屬性生效。但是,這種做法是放棄了Redis
為Hash
集合的記憶體優化。顯然,HSCAN
命令天然不是為了做資料分頁而設計的,而是為了漸進式的迭代(也就是如果需要迭代的集合很大,也不會阻塞Redis
服務)。所以筆者最後放棄了使用HSCAN
命令,尋找更適合做資料分頁查詢的其他Redis
命令。
小結
通過這簡單的踩坑案例,筆者得到一些經驗:
- 切忌先入為主,使用中介軟體的時候要結合實際的場景。
- 使用工具的之前要仔細閱讀工具的使用手冊。
- 要通過一些案例驗證自己的猜想或者推導的結果。
Redis
提供的API十分豐富,後面應該還會遇到更多的踩坑經驗。
附件
- Github Page:www.throwable.club/2019/08/12/…
- Coding Page:throwable.coding.me/2019/08/12/…
- Markdown檔案:github.com/zjcscut/blo…
(本文完 e-a-20190812 c-1-d)