底层数据结构
dict
dict 是一个用于维护 key 和 value 映射关系的数据结构,与很多语言中的 Map 或 dictionary 类似。Redis 的一个 database 中所有 key 到 value 的映射,就是使用一个 dict 来维护的。不过,这只是它在 Redis 中的一个用途而已,它在 Redis 中被使用的地方还有很多。比如,一个 Redis hash 结构,当它的field 较多时,便会采用 dict 来存储。再比如,Redis 配合使用 dict 和 skiplist 来共同维护一个 sorted set。
增量式重哈希:查找、插入、删除都会触发
参考:极客时间 Redis核心技术与实战 笔记(实践篇 数据结构)
SDS
全称 Simple Dynamic String,所有key都是用sds存储的
robj
从 Redis 的使用者的角度来看,一个 Redis 节点包含多个 database (非cluster模式下默认是16个,cluster模式下只能是1个),而一个 database 维护了从 key space 到 object space 的映射关系。这个映射关系的 key 是 string 类型,而 value 可以是多种数据类型,比如:string, list, hash等。我们可以看到,key的类型固定是string,而value可能的类型是多个。
一个database内的这个映射关系是用一个dict来维护的。dict的key固定用一种数据结构来表达就够了,这就是动态字符串sds。而value则比较复杂,为了在同一个dict内能够存储不同类型的value,这就需要一个通用的数据结构,这个通用的数据结构就是robj(全名是redisObject)。举个例子:如果value是一个list,那么它的内部存储结构是一个quicklist(quicklist的具体实现我们放在后面的文章讨论);如果value是一个string,那么它的内部存储结构一般情况下是一个sds。当然实际情况更复杂一点,比如一个string类型的value,如果它的值是一个数字,那么Redis内部还会把它转成long型来存储,从而减小内存使用。而一个robj既能表示一个sds,也能表示一个quicklist,甚至还能表示一个long型。
ziplist
quicklist
参考:Redis内部数据结构详解(5)——quicklist
skiplist
跳表复杂度? O(logn)
intset
intset与ziplist相比:
- ziplist可以存储任意二进制串,而intset只能存储整数。
- ziplist是无序的,而intset是从小到大有序的。因此,在ziplist上查找只能遍历,而在intset上可以进行二分查找,性能更高。
- ziplist可以对每个数据项进行不同的变长编码(每个数据项前面都有数据长度字段len),而intset只能整体使用一个统一的编码(encoding)。
对于小集合使用intset来存储,主要的原因是节省内存。特别是当存储的元素个数较少的时候,dict所带来的内存开销要大得多(包含两个哈希表、链表指针以及大量的其它元数据)。所以,当存储大量的小集合而且集合元素都是数字的时候,用intset能节省下一笔可观的内存空间。
实际上,从时间复杂度上比较,intset的平均情况是没有dict性能高的。以查找为例,intset是O(log n)的,而dict可以认为是O(1)的。但是,由于使用intset的时候集合元素个数比较少,所以这个影响不大。
Redis 五种常见数据类型
String
底层实现:SDS,整数
Hash
底层实现:ziplist,dict
两个 Redis 配置:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
这个配置的意思是说,在如下两个条件之一满足的时候,ziplist会转成dict:
- 当hash中的数据项(即field-value对)的数目超过512的时候,也就是ziplist数据项超过1024的时候(请参考t_hash.c中的hashTypeSet函数)。
- 当hash中插入的任意一个value的长度超过了64的时候(请参考t_hash.c中的hashTypeTryConversion函数)。
hash 和 string 的存储效率
我们在网上很容易找到这样一些技术文章,它们会说存储一个对象,使用 hash 比 string 要节省内存。实际上这么说是有前提的,具体取决于对象怎么来存储。如果你把对象的多个属性存储到多个 key 上(各个属性值存成 string),当然占的内存要多。但如果你采用一些序列化方法,比如 Protocol Buffers,或者 Apache Thrift,先把对象序列化为字节数组,然后再存入到 Redis 的 string 中,那么跟hash 相比,哪一种更省内存,就不一定了。
当然,hash 比序列化后再存入 string 的方式,在支持的操作命令上,还是有优势的:它既支持多个 field 同时存取(hmset/hmget),也支持按照某个特定的 field 单独存取(hset/hget)。
实际上,hash 随着数据的增大,其底层数据结构的实现是会发生变化的,当然存储效率也就不同。在field比较少,各个 value 值也比较小的时候,hash 采用 ziplist 来实现;而随着 field 增多和 value 值增大,hash 可能会变成 dict 来实现。当 hash 底层变成 dict 来实现的时候,它的存储效率就没法跟那些序列化方式相比了。
List
底层实现:3.2 以前是 ziplist 和 linkedlist,3.2以后是 quicklist
Set
底层实现:intset,dict
Sorted Set
底层实现:ziplist,skiplist
如下两个条件之一满足的时候,ziplist会转成zset(具体的触发条件参见t_zset.c中的zaddGenericCommand相关代码):
- 当sorted set中的元素个数,即(数据, score)对的数目超过128的时候,也就是ziplist数据项超过256的时候。
- 当sorted set中插入的任意一个数据的长度超过了64的时候。