解惑:Redis的HSCAN命令中COUNT参数的失效场景
前提
?這是一篇Redis命令使用不當的踩坑經歷分享
?筆者最近在做一個項目時候使用Redis存放客戶端展示的訂單列表,列表需要進行分頁。由于筆者先前對Redis的各種數據類型的使用場景并不是十分熟悉,于是先入為主地看到Hash類型的數據結構,假定:
USER_ID:1ORDER_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屬性描述中找到了原因:
r-h-p-1簡單翻譯理解一下:
SCAN命令以及其衍生命令并不保證每一輪迭代返回的元素數量,但是可以使用COUNT屬性憑經驗調整SCAN命令的行為。COUNT指定每次調用應該完成遍歷的元素的數量,以便于遍歷集合,「本質只是一個提示值」(just a hint,hint意思為暗示)。
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(字典類型編碼是哈希表,即hashtable):
當Hash集合中的數據項(即Field-Value對)的「數目超過512」的時候。
當Hash集合中插入的任意一個Field-Value對中的「Value長度超過64」的時候。
當Hash集合的編碼會由ziplist會轉成dict,Redis為Hash類型的內存空間占用優化相當于失敗了,降級為相對消耗更多內存的字典類型編碼,這個時候,HSCAN命令COUNT屬性才會起效。
案例驗證
?查詢Redis中Key的編碼類型的命令為:object encoding $KEY
?簡單驗證一下上一節得出的結論,寫入一個測試數據如下:
//?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_YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY//?第二輪迭代? HSCAN?USER_ID:2?2?COUNT?1 0?ORDER_ID:ORDER_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX測試案例中故意讓兩個值的長度為70,大于64,也就是讓Hash集合轉變為dict(hashtable)類型,使得COUNT屬性生效。但是,這種做法是放棄了Redis為Hash集合的內存優化。此前驗證的是hash-max-ziplist-value配置項的臨界值,還可以編寫一個例子驗證hash-max-ziplist-entries的臨界值:
//?下面的代碼需要確保本地安裝了Redis,并且引入Redis的客戶端依賴:io.lettuce:lettuce-core:5.3.3.RELEASE public?class?HashScanCountSample?{static?String?KEY?=?"HS";static?int?THRESHOLD?=?513;static?int?COUNT?=?5;public?static?void?main(String[]?args)?throws?Exception?{ScanArgs?scanArgs?=?new?ScanArgs().limit(COUNT);RedisURI?redisUri?=?RedisURI.create("127.0.0.1",?6379);RedisClient?redisClient?=?RedisClient.create(redisUri);RedisCommands<String,?String>?commands?=?redisClient.connect().sync();commands.del(KEY);int?total?=?10;for?(int?i?=?1;?i?<=?total;?i++)?{String?fv?=?String.valueOf(i);commands.hset(KEY,?fv,?fv);}ScanCursor?scanCursor?=?ScanCursor.INITIAL;int?idx?=?1;processScan(total,?scanArgs,?commands,?scanCursor,?idx);for?(int?i?=?11;?i?<=?THRESHOLD;?i++)?{String?fv?=?String.valueOf(i);commands.hset(KEY,?fv,?fv);}scanCursor?=?ScanCursor.INITIAL;total?=?THRESHOLD;idx?=?1;processScan(total,?scanArgs,?commands,?scanCursor,?idx);}private?static?void?processScan(int?total,?ScanArgs?scanArgs,?RedisCommands<String,?String>?commands,?ScanCursor?scanCursor,?int?idx)?{System.out.println(String.format("%d個F-V的HS的編碼:%s",?total,?commands.objectEncoding(KEY)));System.out.println(String.format("%d個F-V的HS進行HSCAN...",?total));MapScanCursor<String,?String>?result;while?(!(result?=?commands.hscan(KEY,?scanCursor,?scanArgs)).isFinished())?{System.out.println(String.format("%d個F-V的HS進行HSCAN第%d次遍歷,size=%d",?total,?idx,?result.getMap().size()));scanCursor?=?new?ScanCursor(result.getCursor(),?result.isFinished());idx++;}System.out.println(String.format("%d個F-V的HS進行HSCAN第%d次遍歷,size=%d",?total,?idx,?result.getMap().size()));} }//?某次輸出結果 10個F-V的HS的編碼:ziplist 10個F-V的HS進行HSCAN... 10個F-V的HS進行HSCAN第1次遍歷,size=10 ...... 513個F-V的HS的編碼:hashtable 513個F-V的HS進行HSCAN... 513個F-V的HS進行HSCAN第1次遍歷,size=5 ...... 513個F-V的HS進行HSCAN第92次遍歷,size=6 513個F-V的HS進行HSCAN第93次遍歷,size=6 513個F-V的HS進行HSCAN第94次遍歷,size=5這里看到,最終遍歷513個F-V的Hash類型的KEY,最多每次能遍歷出9個F-V對,這里只是其中一次的測試數據,也就是說COUNT值即使固定為一個常量,但是遍歷出來的數據集合中的元素數量不一定為COUNT,但是大多數情況下為COUNT。
?不過可以推斷出一點,如果Hash中的F-V對的數量小于512,并且所有的V的長度都比較短,HSCAN命令會一次遍歷出該KEY的所有的F-V對
?顯然,HSCAN命令天然不是為了做數據分頁而設計的,而是為了漸進式的迭代(也就是如果需要迭代的集合很大,也不會一直阻塞Redis服務)。所以筆者最后放棄了使用HSCAN命令,尋找更適合做數據分頁查詢的其他Redis命令。
小結
通過這簡單的踩坑案例,筆者得到一些經驗:
切忌先入為主,使用中間件的時候要結合實際的場景。
使用工具的之前要仔細閱讀工具的使用手冊。
要通過一些案例驗證自己的猜想或者推導的結果。
HSCAN命令中的COUNT屬性的功能和Redis服務的配置項hash-max-ziplist-value、hash-max-ziplist-entries以及KEY的編碼類型息息相關。Redis提供的API十分豐富,這些API的版本兼容性做得十分優秀,后面應該還會遇到更多的踩坑經驗。
(本文完 r-a-2020812 c-2-d 封面來源于動漫《青春之旅》)
總結
以上是生活随笔為你收集整理的解惑:Redis的HSCAN命令中COUNT参数的失效场景的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gps数据处理 java_GPS数据读
- 下一篇: 超简单的页眉页脚设置(摘要、目录页眉不同