Redis Hash類型的坑之單個key中field過多


對投票數據統計的時候發現了Redis Hash類型的一個大坑,單個key中field過多,導致取不出來。特記錄下嘗試解決和探索的過程。

第一階段:問題描述

一個投票類的產品,對單個選項mid投票成功后,記錄了總票數,還記錄了用戶投票日志(可以理解成投票明細),用的都是Redis Hash類型來存儲。投票日志的存儲格式如下:

1
$redis ->hset( 'vote_log' , 'uid|mid|timestamp' , 1);

最近,運營需要統計固定時間段每個選項投票的用戶數,要的比較急,就很快寫了一個腳本
算法:

0、全部取不來
1、第一次遍歷,篩選出mid
2、第二次遍歷,篩選出每個mid對應的uid
3、去重
4、統計數量
運行就報錯:

腳本的使用內存已經設定為1024M了,查看Redis.php中163行使用的是hGetAll獲取全量數據,估計是數據太大,登陸上Redis 使用hlens顯示2700W,確實太大了,改為hKeys獲取還是一樣報錯。Goolge搜索了一番,發現唯一的解決方案就是使用hscan, 測試發現線上redis server版本2.4不支持hscan(hscan命令版本要求>=2.8), 頓時都絕望了,跟運營反饋數據存儲集中,並且數量比較大,傳統的方案統計不出來,需要時間用其他方式來處理。好在,運營了解情況后,表示可以不用統計用戶數。

第二階段:探索解決方案

但是技術上這個問題並沒有解決,如何解決這個問題呢

目前分析思考結果如下:
0、使用內存足夠的機器跑腳本
1、使用hscan[自己的阿里雲上redis server version 3.2.11測試了hscan確實可以分頁取數據]
第一種:升級redis server version到2.8以上
第二種:導出redis key,導入高版本redis server中

具體實施的解決方案,還在探索中…(redis hash key導出在嘗試中,如果大家有好的建議,歡迎留言)

針對hscan這里有一個地方需要格外注意(scan不存在這個問題)

觀察下面幾條命令,我們看到vote_info中現在有4個鍵值對,但是我們設置hscan的count為2,還是返回了全部內容,並不是預期的2條

我們知道Redis Hashes是由ziplist(壓縮列表)和字典(Dict)兩種編碼方式實現,當我們創建一個空的Hashes的時候使用的ziplist編碼, 當某個鍵或某個值的長度大於hash_max_ziplist_value設定的值,會切換的Dict編碼,還有一種情況也會切換就是ziplist的entries(節點數)大於hash_max_ziplist_entries。hash_max_ziplist_value和 hash_max_ziplist_entries在redis.conf中設置,默認值分別是512和64。

hash-max-zipmap-entries 512 (hash-max-ziplist-entries for Redis >= 2.6)
hash-max-zipmap-value 64  (hash-max-ziplist-value for Redis >= 2.6)

查看redis scan 文檔,Hashes使用ziplist編碼的時候,通常忽略count參數,直接返回全部元素。

打開redis.conf, 把hash_max_ziplist_entries修改為10,hset多個元素,直到hlen為11的時候,count才生效,觀察下面一組命令

按照上面的實驗,ziplist中一對key-value算一個entries,沒有找到理論說明, 參考《Redis設計與實現第一版》中的結構

一個ziplist的分布結構:

key-value一同壓入ziplist后的結構:

測試遺留問題:當hdel一條記錄后,hscan的count選項還是生效,返回的數量也有異常,暫未找到原因

第三階段:優化存儲結構,避免問題

去年PHP開發者大會上,記得鳥哥說,避免問題也是一種解決問題的好辦法。

如何存儲來避免這種問題呢
方案一:存儲key的優化, 按照mid來拆分key

1
$redis ->hset( 'vote_log_mid' , 'uid|timestamp' , 1);

方案二:存儲key的優化,按天來存

1
2
3
4
5
// 需要區分投票選項
$redis ->hIncrBy( 'vote_log_mid_20180718' , 'uid' , 1);
 
// 不需要區分投票選項
$redis ->hIncrBy( 'vote_log_20180718' , 'uid' , 1);

方案三:redis2mysql, 直接存到mysql中
–表結構

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `vote_log` (
`id` int (11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID' ,
`aid` int (11) unsigned NOT NULL DEFAULT '0' COMMENT '活動ID' ,
`feed_id` int (11) unsigned NOT NULL DEFAULT '0' COMMENT 'feed id' ,
`mid` bigint (11) unsigned NOT NULL DEFAULT '0' COMMENT 'mid' ,
`uid` bigint (11) unsigned NOT NULL DEFAULT '0' COMMENT 'uid' ,
`create_time` bigint (11) unsigned NOT NULL DEFAULT '0' COMMENT '投票時間' ,
`ext` varchar (64) NOT NULL DEFAULT '' COMMENT '擴展字段' ,
PRIMARY KEY (`id`),
KEY `key_a_f_c` (`aid`,`feed_id`,`create_time`),
KEY `key_a_f_m` (`aid`,`feed_id`,`mid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT= '投票日志' ;

結合實際情況,雖然方案一和方案二改造起來很方便,這個日志數據並不需要實時讀取,放在Redis中有對Redis誤用亂用的嫌疑,也並不方便運營未知的統計需求。存到mysql中開始考慮到按照日期或者活動id來分表,查看發現不是每個月都有這樣的投票活動,也不是每個活動都有投票,完全可以存一張表,等到時候數據太大了,可以寫腳本清歷史數據,或者手動清。

以上,選中了方案三。

其他已知的方案:
拆分key, 參考《Redis單key值過大,優化方式》
單獨存一份field,參考《Redis Hash結構遍歷某一個key下所有field value的方法》

第四階段:思考總結

在解決問題的過程中,發現很多朋友遇到了類似的問題,確實值得我們深思,在當初設計存儲的時候,必須要考慮到這種情況,最好的解決辦法還是設計階段提前預判和規避,世界杯決賽上解說說的,追不上姆巴佩,提前預判他的路線,打斷他的進攻,不給發揮速度的機會,這也是架構設計的意義吧。

最后補充一個有趣的發現,刪除hash總最后一個field后,hash key也會被刪除

1
2
3
4
5
6
7
8
9
10
redis 10.235.25.242:6379> hmset salmonl_20190514 id 100 type 1
OK
redis 10.235.25.242:6379> hdel salmonl_20190514 id
(integer) 1
redis 10.235.25.242:6379> exists salmonl_20190514
(integer) 1
redis 10.235.25.242:6379> hdel salmonl_20190514 type
(integer) 1
redis 10.235.25.242:6379> exists salmonl_20190514
(integer) 0

(全文完)

參考資料:
https://redis.io/topics/memory-optimization
https://redis.io/commands/scan#the-count-option
http://origin.redisbook.com/compress-datastruct/ziplist.html#id2
https://stackoverflow.com/questions/34503876/redis-hscan-command-cannot-limit-the-counts

 

 

原文地址:redis一個hash能存多少field (gxlcms.com)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM