起因
最近在接公司的中台服务,他们提供的sdk代码中有长的redis key前缀,长度达到20 byte,和他们争执了一下key的长度问题,虽然最后他们修改了,但是感觉我这边没有给出足够有说服力的解释,他们心里不服,所以,Talk is Cheap, Show U the Code & Data。
简单分析
简单分析下,redis key的长度会从存储和网络传输两方面带来影响,我们将从源码分析网络传输和存储流程,并最终用benchmark数据做佐证。
网络传输分析
redis client和server间通过RESP(REdis Serialization Protocol)协议进行通信,其兼顾了解析效率和可读性,例如,get foo 命令的TCP包的详情如下:
整个TCP包占74 Bytes(52 Bytes Overhead + 22 Bytes Data),redis命令占用的22个字节:
1 | *2\r\n$3\r\nget\r\n$3\r\nfoo\r\n |
解释如下图:
有一种要打脸的预感😅。
TCP一个包的长度最大时65535 Bytes,但是一般MTU都是1500 Bytes,从这个角度上来说,单条命令或者pipeline组合的多条命令,只要不是太长,都能在一个TCP包里面将数据传输到Server端,二十多个字符前缀的key和缩写后的5个字节前缀的key的差别不会太大,都能在一个TCP包传输完,只有当pipeline命令长度超过一个数据包的大小时,才会带来时间上的差异。
内存使用分析
redis server在redis源代码中用 redisServer 结构体表示,其中包含了 redisDb 这个结构体用于表示db对象,其定义如下:
1 | /* Redis database representation. There are multiple databases identified |
从中我们看到两个比较重要的 dict 结构体分别是 dict和expires,前者是存储reedis的key-value数据的,后者是存储key-expire数据的,也就是每个key的过期时间,dict是redis对hash table的包装,dict结构的定义如下:
1 | typedef struct dict { |
dict中真正存储数据的是 dictht ,这里使用了两个主要是为了方便在 rehash 的时候使用,dicht本质上就是一个hash table:
1 | /* This is our hash table structure. Every dictionary has two of this as we |
dictht在存储的时候先对key做hash,得到一个槽的位置,然后通过开链表的方式将value存储到对应槽的首位。这里dictEntry存储了一个实际的key-value
1 | typedef struct dictEntry { |
可以看到value是union,其实就是为了方便存储各种类型的数据,例如存储过期时间需要使用int64_t,存储double的value需要使用double,而存储字符串需要使用void*
从代码分析,如果key长度太长,确实会占用更多的内存空间,因为redis的key都是直接按照字符串存储的(准确说是sds),对于value是会有编码的,暂不在这里展开。
数据测试
由于redis自带的benchmark工具的测试点有限,目前不能设置key的长度,因此用JedisClient写了简单代码做分析,测试10W个key的写入情况
key | value | memory | cost time(sigle command) | cost time(pipeline) | command per tcp package(pipeline) |
---|---|---|---|---|---|
sp: + (0 - 1W数字) | 5-16随机字符 | 7.77M |
391994ms |
7434ms |
36 |
long_prefix: + (0 - 1W数字) | 5-16随机字符 | 8.63M | 426011ms | 8450ms | 27 |
运行环境:Redis 4.0.0 on Raspberry pi II - 512M RAM
结论
- key的长度。单个命令情况下,耗时基本无影响;在pipeline情况下,如果key较多,每增加一个tcp包大概增加2ms耗时,在我们的测试中,如果一次取200个key,那么耗时大概差两个tcp包,4ms左右,
- 存储空间。会有增加,增加多少得看具体情况,我们的测试中大概增加11%