解惑:Redis的HSCAN命令中COUNT参数的失效场景
前提
?這是一篇Redis命令使用不當(dāng)?shù)牟瓤咏?jīng)歷分享
?筆者最近在做一個(gè)項(xiàng)目時(shí)候使用Redis存放客戶(hù)端展示的訂單列表,列表需要進(jìn)行分頁(yè)。由于筆者先前對(duì)Redis的各種數(shù)據(jù)類(lèi)型的使用場(chǎng)景并不是十分熟悉,于是先入為主地看到Hash類(lèi)型的數(shù)據(jù)結(jié)構(gòu),假定:
USER_ID:1ORDER_ID:ORDER_XX: {"amount": "100","orderId":"ORDER_XX"}ORDER_ID:ORDER_YY: {"amount": "200","orderId":"ORDER_YY"}感覺(jué)Hash類(lèi)型完全滿足需求實(shí)現(xiàn)的場(chǎng)景。然后想當(dāng)然地考慮使用HSCAN命令進(jìn)行分頁(yè),引發(fā)了后面遇到的問(wèn)題。
SCAN和HSCAN命令
SCAN命令如下:
SCAN?cursor?[MATCH?pattern]?[COUNT?count]?[TYPE?type] //?返回值如下: //?1.?cursor,數(shù)值類(lèi)型,下一輪的起始游標(biāo)值,0代表遍歷結(jié)束 //?2.?遍歷的結(jié)果集合,列表SCAN命令在Redis2.8.0版本中新增,時(shí)間復(fù)雜度計(jì)算如下:每一輪遍歷的時(shí)間復(fù)雜度為O(1),所有元素遍歷完畢直到游標(biāo)cursor返回0的時(shí)間復(fù)雜度為O(N),其中N為集合內(nèi)元素的數(shù)量。SCAN是針對(duì)整個(gè)Database內(nèi)的所有KEY進(jìn)行漸進(jìn)式的遍歷,它不會(huì)一直阻塞Redis,也就是使用SCAN命令遍歷KEY的性能有可能會(huì)優(yōu)于KEY *命令。對(duì)于Hash類(lèi)型有一個(gè)衍生的命令HSCAN專(zhuān)門(mén)用于遍歷Hash類(lèi)型及其相關(guān)屬性(Field)的字段:
HSCAN?key?cursor?[MATCH?pattern]?[COUNT?count] //?返回值如下: //?1.?cursor,數(shù)值類(lèi)型,下一輪的起始游標(biāo)值,0代表遍歷結(jié)束 //?2.?遍歷的結(jié)果集合,是一個(gè)映射筆者當(dāng)時(shí)沒(méi)有仔細(xì)查閱Redis的官方文檔,想當(dāng)然地認(rèn)為Hash類(lèi)型的分頁(yè)簡(jiǎn)單如下(偏激一點(diǎn)假設(shè)每頁(yè)數(shù)據(jù)只有1條):
//?第一頁(yè) HSCAN?USER_ID:1?0?COUNT?1????<=?這里認(rèn)為返回的游標(biāo)值為1 //?第二頁(yè) HSCAN?USER_ID:1?1?COUNT?1????<=?這里認(rèn)為返回的游標(biāo)值為0,結(jié)束迭代實(shí)際上,執(zhí)行的結(jié)果如下:
HSCAN?USER_ID:1?0?COUNT?1//?結(jié)果 0?ORDER_ID:ORDER_XX{"amount":?"100","orderId":"ORDER_XX"}ORDER_ID:ORDER_YY{"amount":?"200","orderId":"ORDER_YY"}也就是在第一輪遍歷的時(shí)候,KEY對(duì)應(yīng)的所有Field-Value已經(jīng)全量返回。筆者嘗試增加哈希集合KEY = USER_ID:1里面的元素,但是數(shù)據(jù)量相對(duì)較大的時(shí)候,依然沒(méi)有達(dá)到預(yù)期的分頁(yè)效果;另一個(gè)方面,嘗試修改命令中的COUNT值,發(fā)現(xiàn)無(wú)論如何修改COUNT值都不會(huì)對(duì)遍歷的結(jié)果產(chǎn)生任何影響(也就是還是在第一輪迭代返回全部結(jié)果)。百思不得其解的情況下,只能仔細(xì)翻閱官方文檔尋找解決方案。在SCAN命令的COUNT屬性描述中找到了原因:
r-h-p-1簡(jiǎn)單翻譯理解一下:
SCAN命令以及其衍生命令并不保證每一輪迭代返回的元素?cái)?shù)量,但是可以使用COUNT屬性憑經(jīng)驗(yàn)調(diào)整SCAN命令的行為。COUNT指定每次調(diào)用應(yīng)該完成遍歷的元素的數(shù)量,以便于遍歷集合,「本質(zhì)只是一個(gè)提示值」(just a hint,hint意思為暗示)。
COUNT默認(rèn)值為10。
當(dāng)遍歷的目標(biāo)Set、Hash、Sorted Set或者Key空間足夠大可以使用一個(gè)哈希表表示并且不使用MATCH屬性的前提下,Redis服務(wù)端會(huì)返回COUNT或者比COUNT大的遍歷元素結(jié)果集合。
當(dāng)遍歷只包含Integer值的Set集合(也稱(chēng)為intsets),或者ziplists類(lèi)型編碼的Hash或者Sorted Set集合(說(shuō)明這些集合里面的元素占用的空間足夠小),那么SCAN命令會(huì)返回集合中的所有元素,直接忽略COUNT屬性。
注意第3點(diǎn),這個(gè)就是在Hash集合中使用HSCAN命令COUNT屬性失效的根本原因。Redis配置中有兩個(gè)和Hash類(lèi)型ziplist編碼的相關(guān)配置值:
hash-max-ziplist-entries 512 hash-max-ziplist-value 64在如下兩個(gè)條件之一滿足的時(shí)候,Hash集合的編碼會(huì)由ziplist會(huì)轉(zhuǎn)成dict(字典類(lèi)型編碼是哈希表,即hashtable):
當(dāng)Hash集合中的數(shù)據(jù)項(xiàng)(即Field-Value對(duì))的「數(shù)目超過(guò)512」的時(shí)候。
當(dāng)Hash集合中插入的任意一個(gè)Field-Value對(duì)中的「Value長(zhǎng)度超過(guò)64」的時(shí)候。
當(dāng)Hash集合的編碼會(huì)由ziplist會(huì)轉(zhuǎn)成dict,Redis為Hash類(lèi)型的內(nèi)存空間占用優(yōu)化相當(dāng)于失敗了,降級(jí)為相對(duì)消耗更多內(nèi)存的字典類(lèi)型編碼,這個(gè)時(shí)候,HSCAN命令COUNT屬性才會(huì)起效。
案例驗(yàn)證
?查詢(xún)Redis中Key的編碼類(lèi)型的命令為:object encoding $KEY
?簡(jiǎn)單驗(yàn)證一下上一節(jié)得出的結(jié)論,寫(xiě)入一個(gè)測(cè)試數(shù)據(jù)如下:
//?70個(gè)X HSET?USER_ID:2?ORDER_ID:ORDER_XXX?XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX??? //?70個(gè)Y HSET?USER_ID:2?ORDER_ID:ORDER_YYY?YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY接著開(kāi)始測(cè)試一下HSCAN命令:
//?查看編碼 object?encoding?USER_ID:2 //?編碼結(jié)果 hashtable//?第一輪迭代 HSCAN?USER_ID:2?0?COUNT?1 //?第一輪迭代返回結(jié)果 2?ORDER_ID:ORDER_YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY//?第二輪迭代? HSCAN?USER_ID:2?2?COUNT?1 0?ORDER_ID:ORDER_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX測(cè)試案例中故意讓兩個(gè)值的長(zhǎng)度為70,大于64,也就是讓Hash集合轉(zhuǎn)變?yōu)閐ict(hashtable)類(lèi)型,使得COUNT屬性生效。但是,這種做法是放棄了Redis為Hash集合的內(nèi)存優(yōu)化。此前驗(yàn)證的是hash-max-ziplist-value配置項(xiàng)的臨界值,還可以編寫(xiě)一個(gè)例子驗(yàn)證hash-max-ziplist-entries的臨界值:
//?下面的代碼需要確保本地安裝了Redis,并且引入Redis的客戶(hù)端依賴(lài):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個(gè)F-V的HS的編碼:%s",?total,?commands.objectEncoding(KEY)));System.out.println(String.format("%d個(gè)F-V的HS進(jìn)行HSCAN...",?total));MapScanCursor<String,?String>?result;while?(!(result?=?commands.hscan(KEY,?scanCursor,?scanArgs)).isFinished())?{System.out.println(String.format("%d個(gè)F-V的HS進(jìn)行HSCAN第%d次遍歷,size=%d",?total,?idx,?result.getMap().size()));scanCursor?=?new?ScanCursor(result.getCursor(),?result.isFinished());idx++;}System.out.println(String.format("%d個(gè)F-V的HS進(jìn)行HSCAN第%d次遍歷,size=%d",?total,?idx,?result.getMap().size()));} }//?某次輸出結(jié)果 10個(gè)F-V的HS的編碼:ziplist 10個(gè)F-V的HS進(jìn)行HSCAN... 10個(gè)F-V的HS進(jìn)行HSCAN第1次遍歷,size=10 ...... 513個(gè)F-V的HS的編碼:hashtable 513個(gè)F-V的HS進(jìn)行HSCAN... 513個(gè)F-V的HS進(jìn)行HSCAN第1次遍歷,size=5 ...... 513個(gè)F-V的HS進(jìn)行HSCAN第92次遍歷,size=6 513個(gè)F-V的HS進(jìn)行HSCAN第93次遍歷,size=6 513個(gè)F-V的HS進(jìn)行HSCAN第94次遍歷,size=5這里看到,最終遍歷513個(gè)F-V的Hash類(lèi)型的KEY,最多每次能遍歷出9個(gè)F-V對(duì),這里只是其中一次的測(cè)試數(shù)據(jù),也就是說(shuō)COUNT值即使固定為一個(gè)常量,但是遍歷出來(lái)的數(shù)據(jù)集合中的元素?cái)?shù)量不一定為COUNT,但是大多數(shù)情況下為COUNT。
?不過(guò)可以推斷出一點(diǎn),如果Hash中的F-V對(duì)的數(shù)量小于512,并且所有的V的長(zhǎng)度都比較短,HSCAN命令會(huì)一次遍歷出該KEY的所有的F-V對(duì)
?顯然,HSCAN命令天然不是為了做數(shù)據(jù)分頁(yè)而設(shè)計(jì)的,而是為了漸進(jìn)式的迭代(也就是如果需要迭代的集合很大,也不會(huì)一直阻塞Redis服務(wù))。所以筆者最后放棄了使用HSCAN命令,尋找更適合做數(shù)據(jù)分頁(yè)查詢(xún)的其他Redis命令。
小結(jié)
通過(guò)這簡(jiǎn)單的踩坑案例,筆者得到一些經(jīng)驗(yàn):
切忌先入為主,使用中間件的時(shí)候要結(jié)合實(shí)際的場(chǎng)景。
使用工具的之前要仔細(xì)閱讀工具的使用手冊(cè)。
要通過(guò)一些案例驗(yàn)證自己的猜想或者推導(dǎo)的結(jié)果。
HSCAN命令中的COUNT屬性的功能和Redis服務(wù)的配置項(xiàng)hash-max-ziplist-value、hash-max-ziplist-entries以及KEY的編碼類(lèi)型息息相關(guān)。Redis提供的API十分豐富,這些API的版本兼容性做得十分優(yōu)秀,后面應(yīng)該還會(huì)遇到更多的踩坑經(jīng)驗(yàn)。
(本文完 r-a-2020812 c-2-d 封面來(lái)源于動(dòng)漫《青春之旅》)
總結(jié)
以上是生活随笔為你收集整理的解惑:Redis的HSCAN命令中COUNT参数的失效场景的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: gps数据处理 java_GPS数据读
- 下一篇: linux cmake编译源码,linu