Redis基础数据类型与对象

  • 简单动态字符串
edis底层是使用C语言实现的,但是在redis中并没有直接使用C语言传统的字符串,也就是以空字符结尾的字符串,而是自己构建了一套名为简单动态字符串的抽象结构,simple dynamic string 简称SDS。比如创建一个字符串类型的键值对时,键值对的键和值都是一个SDS对象。
定义:
struct sdshdr{
    int len;//buf中已用字节数量
    int free;//buf中空闲字节数量
    char buf[];//字节数组,保存字符串
}
SDS遵循C字符串空字符结尾的惯例,会自动添加一个空字符在字符串结尾,但不计入len中,也就是这个空字符对使用者是完全透明的。
SDS优点:
1.O(1)复杂度获取字符串长度
2.杜绝缓冲区溢出:假设有两个在内存中紧邻的C字符串s1、s2,如果对s1执行字符串拼接函数,可能将s1的数据溢出至s2度空间中。但SDS结构杜绝这种情况的发生,对SDS修改时,会先检查SDS的空间是否满足要求,不满足的话会进行扩容,再去执行实际的修改。
3.空间分配策略减少内存分配次数:在实际开发中,经常会有类似于上面这种修改字符串长度的操作,SDS不可能每次都去重新分配或释放内存,所以SDS实现了两种空间分配优化策略。
-空间预分配
SDS修改后: len>=1MB free=1MB, len<1MB free=1MB
-惰性空间释放
字符串缩短操作后,不立刻回收内存,使用free属性记录空闲空间长度
4.二进制安全:SDS使用len属性记录字符串的长度,而不是使用空字符判断字符串是否结束。
5.复用一部分C字符串函数库中的函数
  • 链表
链表是一种灵活并且常用的数据结构,但C语言并没有内置这种结构,所以Redis构建了自己的链表实现。
定义:
struct listNode{
    struct listNode *prev;//前置节点
    struct listNode *next;//后置节点
    void *value;//节点值
}               
struct list{
    listNode *head;//表头节点
    listNode *tail;//表尾节点
    unsigned long len;//节点数量
   void *(*dup) (void *ptr);//节点值复制函数
   void *(*free) (void *ptr);//节点值释放函数
   void *(*match) (void *ptr,void *key);//节点值对比函数
}                    
链表的特点:双端、无环、带有头节点和尾节点指针、带有长度计数器、多态(可以根据节点类型设定类型特定函数,所以链表可以保存不同类型的值)
  • 字典:
字典这种结构在Redis中应用十分广泛,Redis的数据库就是使用字典来实现的,并且字典也是哈希键的底层实现之一,这个会在之后的对象部分详细说明,同样的,C语言中没有字典这个结构,Redis构建了自己的字典实现,
定义:
/*哈希表节点*/
struct dictEntry{
    void *key;//
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    }v;//
    struct dictEntry *next;//指向下一个节点的指针
}
/*哈希表*/
struct dictht{
    dictEntry **table;//哈希表数据
    unsigned long size;//哈希表大小
    unsigned long sizemask;//哈希表大小掩码=size-1
    unsigned long used;//已有节点数量
}
struct dict{
    dictType *type;//类型特定函数->实现多态字典
    void *privdate;//私有数据->保存类型特定函数可选参数
    dictht ht[2];//哈希表->ht[1]在rehash时使用
    int trehashidx;//rehash索引-没有进行resash时 值为-1
}
说明:哈希表的大小也就是table数组的大小,掩码与哈希值做&运算决定键应该放在数组的哪个索引上,根据这个结构可以看出Redis的字典使用的是链哈希。
字典使用链地址法解决冲突,并且哈希表的链表没有存储链尾指针,所以为了速度考虑,新加入的节点添加至链表表头位置。
随着哈希表的收缩与扩展,为了让负载因子(负载因子 = 填入表中的元素个数/散列表的长度)维持在一个合理的范围,哈希表要进行rehash,
rehash大体上分为3步:首先根据一定的规则为字典表的ht[1]分配空间,接下来将ht[0]上的键值对重新计算索引值rehash到ht[1]上,最后将ht[1]设为ht[0],并为ht[1]创建一个空白哈希表,Redis字典的rehash是渐进的,ht[0]上的键值对是分多次rehash到ht[1]上的,这样避免了对大量键值rehash时对服务器的性能造成影响。这几张图展示了字典渐进式rehash的过程。还有一点就是,在rehash过程中,如果有对字典的操作,如果要查找一个键,会先在ht[0]上查找,如果找不到再去ht[1]查找,如果要插入一个键,会直接在ht[1]上进行操作,不会再对ht[0]进行任何插入操作。
  • 跳跃表
跳跃表是一种有序的数据结构,加入了一个层的概念,层数越高的元素越少,每次先从高层查找,再逐渐降层,直到定位到合适的位置。实现了通过在每个节点维持多个指向其他节点的指针达到快速访问的目的,是有序集合键的底层实现之一。
/*跳跃表节点*/
struct zskiplistNode{
    struct zskiplistNode *backward;//后退指针
    double score;//分值
    robj *obj;//成员对象
    struct zskiplistLevel{//
        struct zskiplistNode *forward;//前进指针
        unsigned int span;//跨度
    }level[];
}
/*跳跃表*/
struct zskiplist{
    struct zskiplistNode *header, *tail;//表头节点、表尾节点
    unsigned long length;//节点的数量(表头节点不计入)
    int level;//表中层数最大节点的层数(表头节点不计入)
}
跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,一般来说,层的数量越多,访问其他节点的速度越快。每次创建一个新的跳跃表节点时,会随机生成一个1~32之间的数作为节点的层数。
跨度记录的是两个节点的距离,并且可以计算排位,在查找某个节点的过程中,把访问过的所有层的跨度累计起来,就是这个节点在跳跃表之中的排位。
跳跃表中的每个节点按分值由小到大来排序,每个节点的值都是唯一的,但多个节点的分值可以相同,相同分值的节点按节点值在字典中排序的大小进行排序。
 
  • 整数集合
整数集合是Redis用于保存整数值的集合抽象数据结构,是集合键的底层实现之一。可以保存类型为int16_t,int32_t,int64_t的整数值,并且可以保证集合能不会出现重复元素。
struct intset{
    unit32_t encoding;//编码方式
    unit32_t length;//元素数量
    int8_t contents[];//保存元素的数组
}
整数集合的每一个元素都是contents中的一个数据项,虽然contents的类型为int8_t,但是实际上不保存任何int8_t类型的数据,真正的类型取决于encoding属性,如果encoding属性为int16,那么contents中每一个元素的类型都为int16,如果encoding属性为int64,那么contents中每一个元素的类型都为int64。
整数集合具有升级机制,如果向一个int16的集合添加一个int64类型的元素,那么集合中每个元素的类型都会升级至int64,并且整数集合不支持降级操作,一旦升级,将一直保存升级后的状态。
  • 压缩列表
压缩列表是Redis为了节约内存而开发的、由一系列特殊编码的连续内存块组成的顺序型数据结构,是列表键和哈希键的底层实现之一。一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或一个整数值。
previous_entry_length记录了前一个节点的长度,所以可以根据当前节点的起始地址计算出前一个节点的起始位置,压缩列表的从表尾到表头的遍历操作就是应用这个原理实现的。
 
Redis数据库的键和值都是一个对象,也就是每当我们创建一个键值对时,都至少创建两个对象,键对象和值对象,键对象总是一个字符串对象,而值对象可以是五中对象类型中的一种,每个对象至少使用了一种基础数据结构。
redis的每个对象都由一个redisObject结构表示,下面展示了redisObject与结构相关的属性。
  • 对象的类型与编码
/*Redis对象结构*/
struct redisObject{
    unsigned type:4;//类型
    unsigned encoding:4;//编码
    void *ptr;//指向底层实现数据结构的指针
    ...
}
 
类型常量
对象的名称
TYPE命令输出
REDIS_STRING
字符串对象
"string"
REDIS_LIST
列表对象
"list"
REDIS_HASH
哈希对象
"hash"
REDIS_SET
集合对象
"set"
REDIS_ZSET
有序集合对象
"zset"
TYPR key 命令查看键的值对象的类型;
对象使用的底层数据结构
编码常量
OBJECT ENCODING命令输出
整数
REDIS_ENCODING_INT
"int"
embstr编码的简单动态字符串
REDIS_ENCODING_EMBSTR
"embstr"
简单动态字符串
REDIS_ENCODING_RAW
"raw"
字典
REDIS_ENCODING_HT
"hashtable"
双端链表
REDIS_ENCODING_LINKEDLIST
"linkedlist"
压缩列表
REDIS_ENCODING_ZIPLIST
"ziplist"
整数集合
REDIS_ENCODING_INTSET
"intset"
跳跃表和字典
REDIS_ENCODING_SKIPLIST
"skiplist"
OBJECT ENCODING key 命令查看数据库键的值对象的编码;
  • 字符串对象
embstr是保存短字符串一种优化编码的方式,和raw结构类似,只是分配内存的方式不同,并且redis没有操作embstr编码对象的函数,所以对embstr编码对象进行修改操作时,redis会先将这个对象转换成一个raw编码的对象。
字符串对象也是唯一一种会被其他四种对象嵌套的对象
  • 列表对象
  • 哈希对象
使用压缩列表作为底层实现的哈希对象,每当有新的键值对加入时,先将保存了键的列表节点推入压缩列表表尾,再将保存了值的节点推入表尾,保证同一键值对的节点挨在一起,当进行查找操作时,会先定位到键的位置,再根据键的位置定位值的位置。
  • 集合对象
使用hashtable作为底层实现的集合对象,其字典的每一个键都代表一个集合元素,而字典的每一个值都是null
  • 有序集合对象
使用压缩列表作为底层实现的有序集合,集合的每一个元素使用紧挨在一起的两个压缩列表节点表示,第一个节点保存元素的成员,第二个节点保存分值,并且列表内的集合元素按照分值大小来排序。
使用skiplist作为底层实现的有序集合,同时包含一个字典和一个跳跃表。每个跳跃表节点都保存了一个集合元素,跳跃表节点的object属性保存节点的成员,score属性保存节点的分数。字典为有序集合创建了一个成员到分值到映射,的每个键值对保存了一个集合元素,键保存集合元素的成员,值保存集合元素的分数。跳表提供对有序集合范围型操作的支持,字典支持常数时间复杂度查找元素的分值。虽然有序集合同时使用跳表和字典保存列表元素,但会通过指针共享元素的成员与分值,并不会对内存产生浪费。
 


原文地址:https://www.cnblogs.com/youtang/p/14443982.html