redis實踐及思考
導語:當面臨儲存選型時是選擇關係型還是非關係型資料庫? 如果選擇了非關係型的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位元組的記憶體塊
-
一個value,5位元組,所以SDS(value)需要5+9=14個位元組,jemalloc會分配16位元組的記憶體塊
綜上,一個dictEntry需要32+16+16+16=80個位元組。
需求特點
筆者這個需求背景讀多寫少,冷資料佔比比較大,但資料結構又很複雜(涉及多個維度資料總和),因此只要啟動定時任務離線增量寫入redis,請求到達時直接讀取redis中的資料,無疑可以減少響應時間。
redis瓶頸和最佳化
HGETALL
最終儲存到redis中的資料結構如下圖。
採用同步的方式對三個月(90天)進行HGETALL操作,每一天花費30ms,90次就是2700ms! redis操作讀取應該是ns級別的,怎麼會這麼慢? 利用多核cpu計算會不會更快?
pipeline
於是我把程式碼改了一版,原來是90次I/O,現在透過redis pipeline操作,一次請求半個月,那麼3個月就是6次I/O。 很開心,時間一下子少了1000ms。
pipeline攜帶的命令數
我使用是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的關係,來看一下吞吐量和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操作)
redis cluster
github.com/go-redis就是這樣做的,有興趣可以閱讀下原始碼。
codis
總結
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559354/viewspace-2654502/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Redis原理及實踐之GeoHashRedis
- Redis Pipelining 底層原理分析及實踐Redis
- Redis 非同步客戶端選型及落地實踐Redis非同步客戶端
- vue專案實踐思考003Vue
- Redis使用與實踐Redis
- [譯] 思考實踐:用 Go 實現 FlutterGoFlutter
- 實踐和思考的重要意義
- Bool型SSRF的思考與實踐
- Spring Data Redis 最佳實踐!SpringRedis
- Redis 實戰 —— 02. Redis 簡單實踐 - 文章投票Redis
- WebService安全機制的思考與實踐Web
- 基於 GraphQL 實踐的一點思考
- 前端同構渲染的思考與實踐前端
- Redis 實戰 —— 03. Redis 簡單實踐 - Web應用RedisWeb
- 飛豬Flutter技術演進及業務改造的實踐與思考總結Flutter
- Redis Cluster深入與實踐(續)Redis
- Redis主從同步配置實踐Redis主從同步
- Dockerfile 實踐及梳理Docker
- 搜尋引擎分散式系統思考實踐分散式
- 關於主資料的實踐和思考
- 前端程式碼質量的思考與實踐前端
- 【溫故】前端工程化思考與實踐前端
- 對HashMap的思考及手寫實現HashMap
- Redis核心原理與實踐--事務實踐與原始碼分析Redis原始碼
- Redis叢集環境搭建實踐Redis
- Redis在秒殺功能的實踐Redis
- Redis威脅流量監聽實踐Redis
- 攜程 Redis On Rocks 實踐,節省 2/3 Redis成本Redis
- 壓力測試redis redis-benchmark 優化實踐Redis優化
- Docker Compose 實踐及梳理Docker
- [MySql]explain用法及實踐MySqlAI
- Docker 實踐及命令梳理Docker
- Java泛型及實踐Java泛型
- 數字化轉型的思考與新實踐
- Flutter 動態化熱更新的思考與實踐Flutter
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- Redis叢集slot遷移改造實踐Redis
- kubernetes生產實踐之redis-clusterRedis