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字节的内存块
综上,一个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的变化,从而选择一个适合业务需求的值。
packagemainimport("crypto/rand""fmt""math/big""strconv""time""github.com/garyburd/redigo/redis")constredisKey="redis_test_key:%s"funcmain(){fori:=1;i<10000;i++{testRedisHGETALL(getPreKeyAndLoopTime(i))}}functestRedisHGETALL(keyList[][]string){Conn,err:=redis.Dial("tcp","127.0.0.1:6379")iferr!=nil{fmt.Println(err)return}costTime:=int64(0)start:=time.Now().Unix()for_,keys:=rangekeyList{for_,key:=rangekeys{Conn.Send("HGETALL",fmt.Sprintf(redisKey,key))}Conn.Flush()}end:=time.Now().Unix()costTime=end-startfmt.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的长度,设置不同的循环次数,平均计算,取除网络延迟带来的影响funcgetPreKeyAndLoopTime(keyLenint)[][]string{loopTime:=1000ifkeyLen<10{loopTime*=100}elseifkeyLen<100{loopTime*=50}elseifkeyLen<500{loopTime*=10}elseifkeyLen<1000{loopTime*=5}returngenerateKeys(keyLen,loopTime)}funcgenerateKeys(keyLen,looTimeint)[][]string{keyList:=make([][]string,0)fori:=0;i<looTime;i++{keys:=make([]string,0)fori:=0;i<keyLen;i++{result,_:=rand.Int(rand.Reader,big.NewInt(100))keys=append(keys,strconv.FormatInt(result.Int64(),10))}keyList=append(keyList,keys)}returnkeyList}扩展 (分布式方案下pipeline操作)
github.com/go-redis就是这样做的,有兴趣可以阅读下源码。
总结
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。