Redis基础篇数据结构

数据类型

https://redis.io/topics/data-types-intro

  • String:二进制安全字符串。
  • Lists列表:根据插入顺序排序的字符串元素的集合。它们基本上是链表
  • Sets集合:唯一,未排序的字符串元素的集合。
  • Sorted sets排序集合,类似于集合,但是每个字符串元素都与一个称为score的浮点数字值相关联。元素总是按照它们的分数排序,因此与Sets不同,可以检索一系列元素(例如,您可能会问:给我前10名或后10名)。
  • Hashes哈希,是由与值相关联的字段组成的映射。字段和值都是字符串。这与Ruby或Python哈希非常相似。
  • Bit arrays位数组(或简称为位图):可以使用特殊命令像位数组一样处理字符串值:您可以设置和清除单个位,计数所有设置为1的位,找到第一个设置或未设置的位,等等。
  • HyperLogLogs:这是一个概率数据结构,用于估计集合的基数。别害怕,它比看起来更简单...请参阅本教程的HyperLogLog部分。

数据结构

Redis 是 KV 的数据库,它是通过 hashtable 实现的(我们把这个叫做外层的哈希)。所以每个键值对都会有一个 dictEntry(源码位置:dict.h), 里面指向了 key 和 value 的指针。next 指向下一个 dictEntry

typedef struct dictEntry {
    void *key; /* key 关键字定义 */
    union {
        void *val; uint64_t u64; /* value 定义 */
        int64_t s64; double d;
    } v;
    struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry;

key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在自定义的 SDS(Simple Dynamic String 简单动态字符串) 中。 value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在 redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储 的。 

为什么 Redis 要用 SDS 实现字符串 ?

下面是sds的源码定义:

/* sds.h */
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 当前字符数组的长度 */
    uint8_t alloc; /*当前字符数组总共分配的内存大小 */
    unsigned char flags; /* 当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等 */
    char buf[]; /* 字符串真正的值 */
};

C 语言本身没有字符串类型(只能用字符数组 char[]实现)

  • 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
  • 如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。
  • C 字符串长度的变更会对字符数组做内存重分配。
  • 通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保 存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全

SDS 的特点:

  • 不用担心内存溢出问题,如果需要会对 SDS 进行扩容。
  • 获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。
  • 通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多 次重分配内存。
  • 判断是否结束的标志是 len 属性(它同样以'\0'结尾是因为这样就可以使用 C语言中函数库操作字符串的函数了),可以包含'\0'。

redisObject

redisObject 定义在 src/server.h 文件中。

typedef struct redisObject {
    unsigned type:4; /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
    unsigned encoding:4; /* 具体的数据结构 */
    unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关 */
    int refcount; /* 引用计数。当 refcount 为 0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
    void *ptr; /* 指向对象实际的数据结构 */
} robj;

 字符串类型的内部编码(encoding字段)有三种:

1、int,存储 8 个字节的长整型(long,2^63-1)。

2、embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串), 存储小于 44 个字节的字符串。

3、raw,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)。

embstr 和 raw 的区别?

embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw 需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。

因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次 空间,以及对象的所有数据连在一起,寻找方便。

而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和 SDS 都需要重新分配空间

int 和 embstr 什么时候转化为 raw?

当 int 数据不再是整数 , 或大小超过了 long 的 范围 (2^63-1=9223372036854775807)时,自动转化为 embstr。因此为Redis 中的 embstr 实现为只读。因此在对 embstr 对象进行修改时,都会先 转化为 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节

当长度小于阈值时,会还原吗?

编码转换在 Redis 写入数据时完 成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)。

Hash存储(实现)原理

内层的哈希底层可以使用两种数据结构实现:

ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)

hashtable:OBJ_ENCODING_HT(哈希表)

ziplist

ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一 个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能, 来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面

typedef struct zlentry {
    unsigned int prevrawlensize; /* 上一个链表节点占用的长度 */
    unsigned int prevrawlen; /* 存储上一个链表节点的长度数值所需要的字节数 */
    unsigned int lensize; /* 存储当前链表节点长度数值所需要的字节数 */
    unsigned int len; /* 当前链表节点占用的长度 */
    unsigned int headersize; /* 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小 */
    unsigned char encoding; /* 编码方式 */
    unsigned char *p; /* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置 */
} zlentry;

什么时候使用 ziplist 存储?

1)所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母 一个字节)

 2)哈希对象保存的键值对数量小于 512 个。

 src/redis.conf 配置 

  • hash-max-ziplist-value 64 // ziplist 中最大能存放的值长度
  • hash-max-ziplist-entries 512 // ziplist 中最多能存放的 entry 节点数量

hashtable(dict)

在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。 源码位置:dict.h

Redis 的 KV 结构是通过一个 dictEntry 来实现的。 Redis 又对 dictEntry 进行了多层的封装

从最底层到最高层 dictEntry——dictht——dict——OBJ_ENCODING_HT

为什么要定义两个哈希表呢?

redis 的 hash 默认使用的是 ht[0],ht[1]不会初始化和分配空间。 哈希表 dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决 于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率:

static int dict_can_resize = 1; static unsigned int dict_force_resize_ratio = 5;
dict_can_resize 为 1 并且 dict_force_resize_ratio 已使用节点数和字典大小之间的比率超过 1:5,触发扩容

List 列表

存储有序的字符串(从左到右),元素可以重复。可以充当队列和栈的角色。

存储(实现)原理

3.2 版本之后,统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点 都是一个 ziplist

typedef struct quicklist {
    quicklistNode *head; /* 指向双向列表的表头 */
    quicklistNode *tail; /* 指向双向列表的表尾 */
    unsigned long count; /* 所有的 ziplist 中一共存了多少个元素 */
    unsigned long len; /* 双向链表的长度,node 的数量 */
    int fill : 16; /* fill factor for individual nodes */
    unsigned int compress : 16; /* 压缩深度,0:不压缩; */
} quicklist;

redis.conf 相关参数
 

参数: 含义

list-max-ziplist-size(fill) :正数表示单个 ziplist 最多所包含的 entry 个数。 负数代表单个 ziplist 的大小,默认 8k。 -1:4KB;-2:8KB;-3:16KB;-4:32KB;-5:64KB

list-compress-depth(compress): 压缩深度,默认是 0。 1:首尾的 ziplist 不压缩;2:首尾第一第二个 ziplist 不压缩,以此类推

Set 集合

String 类型的无序集合,最大存储数量 2^32-1(40 亿左右)。

存储(实现)原理

Redis 用 intset 或 hashtable 存储 set。如果元素都是整数类型,就用 inset 存储。 如果不是整数类型,就用 hashtable(数组+链表的存来储结构)如果元素个数超过 512 个,也会用 hashtable 存储。 配置文件

redis.conf set-max-intset-entries 512

ZSet 有序集合

sorted set,有序的 set,每个元素有个 score。 score 相同时,按照 key 的 ASCII 码排序

存储(实现)原理

同时满足以下条件时使用 ziplist 编码:  元素数量小于 128 个  所有 member 的长度都小于 64 字节

对应 redis.conf 参数:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

超过阈值之后,使用 skiplist+dict 存储

typedef struct zskiplistNode {
    sds ele; /* zset 的元素 */
    double score; /* 分值 */
    struct zskiplistNode *backward; /* 后退指针 */
    struct zskiplistLevel {
    struct zskiplistNode *forward; /* 前进指针,对应 level 的下一个节点 */
    unsigned long span; /* 从当前节点到下一个节点的跨度(跨越的节点数) */
} level[]; /* 层 */
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; /* 指向跳跃表的头结点和尾节点 */
    unsigned long length; /* 跳跃表的节点数 */
    int level; /* 最大的层数 */
} zskiplist;

举例:

在这样一个链表中,如果我们要查找某个数据,那么需要从头开始逐个进行比较, 直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点为止(没找到)。 也就是说,时间复杂度为 O(n)。同样,当我们要插入新数据的时候,也要经历同样的查 找过程,从而确定插入位置

这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半 (上图中是 7, 19, 26)。在插入一个数据的时候,决定要放到那一层,取决于一个算法 (在 redis 中 t_zset.c 有一个 zslRandomLevel 这个方法)。

我们想查找 23,查找的路径是沿着下图中标红的指针所指向的方向进行的:先沿着这个新链表进行查找。当碰到比待查数 据大的节点时,再回到原来的链表中的下一层进行查找。需要比较的节点数大概只有原来的一半。这就是跳跃表

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值