您好,登錄后才能下訂單哦!
導語:當面臨存儲選型時是選擇關系型還是非關系型數據庫? 如果選擇了非關系型的redis,redis常用數據類型占用內存大小如何估算的? redis的性能瓶頸又在哪里?
背景
為什么選擇redis
以redis一組K-V為例(”hello” -> “world”),一個簡單的set命令最終會產生4個消耗內存的結構。
關于Redis數據存儲的細節,又要涉及到內存分配器(如jemalloc),簡單說就是存儲170字節,其實內存分配器會分配192字節存儲。
那么總的花費就是
一個dictEntry,24字節,jemalloc會分配32字節的內存塊
一個redisObject,16字節,jemalloc會分配16字節的內存塊
一個key,5字節,所以SDS(key)需要5+9=14個字節,jemalloc會分配16字節的內存塊
綜上,一個dictEntry需要32+16+16+16=80個字節。
筆者這個需求背景讀多寫少,冷數據占比比較大,但數據結構又很復雜(涉及多個維度數據總和),因此只要啟動定時任務離線增量寫入redis,請求到達時直接讀取redis中的數據,無疑可以減少響應時間。
redis瓶頸和優化
最終存儲到redis中的數據結構如下圖。
采用同步的方式對三個月(90天)進行HGETALL操作,每一天花費30ms,90次就是2700ms! redis操作讀取應該是ns級別的,怎么會這么慢? 利用多核cpu計算會不會更快?
于是我把代碼改了一版,原來是90次I/O,現在通過redis pipeline操作,一次請求半個月,那么3個月就是6次I/O。 很開心,時間一下子少了1000ms。
我使用是golang的 redisgo 的客戶端,翻閱源碼發現,redisgo執行pipeline邏輯是 把命令和參數寫到golang原生的bufio中,如果超過bufio默認最大值(4096字節),就發起一次I/O,flush到內核態。
redisgo編碼pipeline規則
如下圖,
*表示后面參數加命令的個數,$表示后面的字符長度
,一條HGEALL命令實際占45字節。
那其實90天數據,一次I/O就可以搞定了(90 * 45 < 4096字節)!
果然,又快了1000ms,耗費時間達到了1秒以內
簡單寫了一個壓測程序,通過比較請求量和qps的關系,來看一下吞吐量和qps的變化,從而選擇一個適合業務需求的值。
package main import ( "crypto/rand" "fmt" "math/big" "strconv" "time" "github.com/garyburd/redigo/redis" ) const redisKey = "redis_test_key:%s" func main() { for i := 1; i < 10000; i++ { testRedisHGETALL(getPreKeyAndLoopTime(i)) } } func testRedisHGETALL(keyList [][]string) { Conn, err := redis.Dial("tcp", "127.0.0.1:6379") if err != nil { fmt.Println(err) return } costTime := int64(0) start := time.Now().Unix() for _, keys := range keyList { for _, key := range keys { Conn.Send("HGETALL", fmt.Sprintf(redisKey, key)) } Conn.Flush() } end := time.Now().Unix() costTime = end - start fmt.Printf("cost_time=[%+v]ms,qps=[%+v],keyLen=[%+v],totalBytes=[%+v]", 1000*int64(len(keyList))/costTime, costTime/int64(len(keyList)), len(keyList), len(keyList)*len(keyList[0])*len(redisKey)) } //根據key的長度,設置不同的循環次數,平均計算,取除網絡延遲帶來的影響 func getPreKeyAndLoopTime(keyLen int) [][]string { loopTime := 1000 if keyLen < 10 { loopTime *= 100 } else if keyLen < 100 { loopTime *= 50 } else if keyLen < 500 { loopTime *= 10 } else if keyLen < 1000 { loopTime *= 5 } return generateKeys(keyLen, loopTime) } func generateKeys(keyLen, looTime int) [][]string { keyList := make([][]string, 0) for i := 0; i < looTime; i++ { keys := make([]string, 0) for i := 0; i < keyLen; i++ { result, _ := rand.Int(rand.Reader, big.NewInt(100)) keys = append(keys, strconv.FormatInt(result.Int64(), 10)) } keyList = append(keyList, keys) } return keyList }
擴展 (分布式方案下pipeline操作)
github.com/go-redis就是這樣做的,有興趣可以閱讀下源碼。
總結
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。