1. 程式人生 > 程式設計 >使用Redis的HSCAN命令遇到的一個問題

使用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命令在Redis2.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指定每次呼叫應該完成遍歷的元素的數量,以便於遍歷集合,本質只是一個提示值。

  1. COUNT預設值為10。
  2. 當遍歷的目標SetHashSorted Set或者Key空間足夠大可以使用一個雜湊表表示並且不使用MATCH屬性的前提下,Redis服務端會返回COUNT或者比COUNT大的遍歷元素結果集合。
  3. 當遍歷只包含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會轉成dictRedisHash型別的記憶體空間佔用優化相當於失敗了,降級為相對消耗更多記憶體的字典型別編碼,這個時候,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屬性生效。但是,這種做法是放棄了RedisHash集合的記憶體優化。顯然,HSCAN命令天然不是為了做資料分頁而設計的,而是為了漸進式的迭代(也就是如果需要迭代的集合很大,也不會阻塞Redis服務)。所以筆者最後放棄了使用HSCAN命令,尋找更適合做資料分頁查詢的其他Redis命令。

小結

通過這簡單的踩坑案例,筆者得到一些經驗:

  • 切忌先入為主,使用中介軟體的時候要結合實際的場景。
  • 使用工具的之前要仔細閱讀工具的使用手冊。
  • 要通過一些案例驗證自己的猜想或者推導的結果。

Redis提供的API十分豐富,後面應該還會遇到更多的踩坑經驗。

附件

(本文完 e-a-20190812 c-1-d)