业务假设,需要保存100w个K-V值。其中K是一个long类型,V是一个String类型。
defwrite100wkv_with_pipline():print("beginwrite100w")pip=client.pipeline(transaction=False)before=getRedisUsedMemory()forkinrange(0,1000000):pip.set(k,'v'+str(k))pip.execute()after=getRedisUsedMemory()print("endwrite100w,using:"+str(after-before))在这个过程中,必须开启pipeline去进行通信。
beginwrite100wbefore:used_memory_human:1.87Mbefore:used_memory_human:70.90Mendwrite100w,using:72380608100w行的k-v数据,使用了约70m的存储空间。
我尝试实用keys对Redis的keys进行一次遍历,结果需要查询9s。
....999998)"301947"999999)"869004"1000000)"524681"(9.03s)2.2Redis的String存储原理分析按照上述统计,一个KV大概所消耗的内存为72字节。那存储的部分都是哪些内容呢?
Redis在存储String的时候,使用的是动态字符串SDS结构,一个对象大概需要如下三个部分:
除了SDS的空间使用外,还需要占用一个RedisObject对象。
//redis源码:server.h:620typedefstructredisObject{unsignedtype:4; 4个bitunsignedencoding:4; 4个bitunsignedlru:LRU_BITS;/*lrutime(relativetoserver.lruclock)*/24个bitintrefcount;32bitvoid*ptr; 64bit}robj这个对象已经需要占用16字节。示意图如下
一方面,当保存的是Long类型整数时,RedisObject中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。另一方面,当保存的是字符串数据,并且字符串小于等于44字节时,RedisObject中的元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片。
小结一下,我们已经计算来SDS(动态字符串)+redisObject两个对象的数据,先假设大约为40Byte,我们剩下计算32bit的数据。
Redis本质上也是一个大的Hash表,也就是说每增加一个对象,就需要增加一个如红箭头标记的一个对象。Redis源码将其命名为:dictEntry
typedefstructdictEntry{void*key;void*val;structdictEntry*next;}dictEntry从源码上看,一个有三个指针,因为使用的是jemalloc内存分配库,所以其会分配32字节,而不是24字节。
至此,就可以比较完整地统计好数据存储的位置了。
从此可见,有效的KV信息其实并不长,假如是两个Long类型的数据,有效的信息只有16字节。Redis设计来一种新的底层存储结构,压缩列表(ziplist),本质上就是将随机指针地址转换为连续地址,通过偏移量进行识别,完成存储。随机寻址和连续寻址也是计算机中最底层的两种技术选型。
使用连续寻址,就可以将多个KV,共同存储在一个dictEntry上。
假如存储的数据就是K-V值,K做一次拆分。譬如每1000个为个Hash集合,采用Hash数据进行存储。本质上,这是一个分治思想,分级存储。
defwrite100wkv_with_pipline_hash():print("beginwrite100winhashway")pip=client.pipeline(transaction=False)before=getRedisUsedMemory()forkinrange(0,1000000):pip.hset(int(k/1000),k%1000,'v'+str(k))pip.execute()after=getRedisUsedMemory()print("endwrite100w,using:"+str(after-before)beginwrite100winhashwayused_memory_human:865.74Kused_memory_human:53.82Mendwrite100w,using:55544192在默认配置下,hash存储这批数据,需要53M,比72M优化来一部分。由于切分过程中,可以保证每个Hash的KV数量为1000,且KV值大小是一致的。所以可以增加以下配置,保证Hash一直使用ziplist做底层数据存储结构。
#表示用压缩列表保存时哈希集合中的最大元素个数。hash-max-ziplist-entries1000#表示用压缩列表保存时哈希集合中单个元素的最大长度hash-max-ziplist-value64这两个配置可以保证本次存储一致保持在ziplist存储。这样空间利用率是最高效的。存储如下,大概比第一版的存储节省5倍的空间。
beginwrite100winhashwayused_memory_human:865.13Kused_memory_human:14.37Mendwrite100w,using:141853443.实验结论3.1Redis的新认知这是第一次针对Redis实践。结合Redis的源码,分析了Redis的数据对象存储情况。Redis的难点并不在业务,而是在于高效,Redis的接口是简单的。
这次实验说明了,假如认真对业务分析,再结合Redis的数据结构和参数进行定制,是可以获得量级优化的。