1、String
redis里的字符串是可以修改的字符串,在内存中以字节数组的形式存在。它的数据结构叫做SDS(Simple Dynamic String)
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
有点类似ArrayList, 可以动态扩容。支持append操作,append时空间满了就新创建一个数组,把老的内容复制过去,再进行append。
Redis 规定字符串的长度不得超过 512M 字节。创建字符串时 len 和 capacity 一样
长,不会多分配冗余空间,这是因为绝大多数场景下我们不会使用 append 操作来修改字符串。 字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。
- Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44个字节 时,使用 raw 形式存储。(44=64-16(对象头)-4(SDS最小空间))
2、Hash
基本结构: redis里的hash采用dict结构,事实上整个redis的所有key和value也组成了一个dict,还有过期key集合,zset中的value和score也都组成了dict, set也是用dict实现,value是null。
它和Java的HashMap类似,只不过键值只能是字符串。还有扩容也和HashMap不一样,使用的是渐进式rehash策略。dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。扩容缩容时,需要分配新的 hashtable,进行搬迁。查询时会同时查询两个 hash 结构,在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。扩容条件:正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),也会强制扩容。
缩容条件:元素个数低于数组长度的 10%就缩容,不考虑 Redis 是否正在做 bgsave。
3、List
-
基本结构: redis里的list是链表,增删快查询慢。早期版本中,在list元素较少的时候,采用ziplist压缩列表来存储,元素较多时采用linkedlist。考虑到linkedlist的prev和next指针要占去16字节,另外每个节点的内存都是单独分配,会加剧内存的碎片化。后续版本对列表数据结构进行了改造,使用quicklist(快速列表)代替了 ziplist 和 linkedlist。
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
quicklist 默认的压缩深度是 0,也就是不压缩。压缩的实际深度由配置参数 list-compress-depth 决定。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。
4、Set
set类似java的HashSet,上面提过是由dict实现,value是null。
5、ZSet
基本结构:ZSet有序列表为每个value赋予一个score权重,用来实现排序。如果 score 值相同还需要再比较 value 值 (字符串比较)。
Redis 的 zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获取 value 列表的功能,这就需要另外一个结构skiplist跳跃列表。-
跳跃列表:
上图就是跳跃列表的示意图,图中只画了四层,Redis 的跳跃表共有 64 层,意味着最多可以容纳 2^64 次方个元素。每一个 kv 块对应的结构如下面的代码中的 zslnode 结构,kv header 也是这个结构,只不过 value 字段是 null 值——无效的,score是 Double.MIN_VALUE,用来垫底的。kv 之间使用指针串起来形成了双向链表结构,它们是 有序 排列的,从小到大。不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。
struct zslnode {
string value;
double score;
zslnode*[] forwards; // 多层连接指针
zslnode* backward; // 回溯指针
}
struct zsl {
zslnode* header; // 跳跃列表头指针
int maxLevel; // 跳跃列表当前的最高层
map<string, zslnode*> ht; // hash 结构的所有键值对
}
1. 查找过程
如图所示,我们要定位到那个紫色的 kv,需要从 header 的最高层开始遍历找到第一个节点 (最后一个比「我」小的元素),然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最后一个比我「小」的元素)。
2. 随机层数
对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数。直观上期望的目标是 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层 2^-63,因为这里每一层的晋升概率是 50%。不过 Redis 标准源码中的晋升概率只有 25%。。所以官方的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一点。
3. 新增过程
首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就可以开始创建新节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。
4. 删除过程
删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数 maxLevel。
5. 关于排名
Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 span (跨度)属性,表示从前一个节点沿着当前层的 forward 指针跳到当前这个节
点中间会跳过多少个节点,在插入删除操作时更新 span 值。
这样当我们要计算一个元素的排名时,只需要将「搜索路径」上的经过的所有节点的跨度 span 值进行叠加就可以算出元素的最终 rank 值。
参考资料来源:《Redis 深度历险: 核心原理和应用实践》