zoukankan      html  css  js  c++  java
  • Redis 系列(02)数据结构

    Redis 系列(02)数据结构

    Redis 系列目录

    1. String

    1.1 基本操作

    mset str 2673 jack 666
    setnx str
    incr str
    incrby str 100
    decr str
    decrby str 100
    set f 2.6
    incrbyfloat f 7.3
    mget str jack
    strlen str
    append str good
    getrange str 0 8
    

    1.2 数据结构

    String 字符串类型的内部编码有三种:

    1. int,存储8个字节的长整型(long,2^63-1)。
    2. embstr SDS(Simple Dynamic String),存储小于44 个字节的字符串。。
    3. raw SDS,存储大于 44 个字节的字符串。

    数据结构示例:

    127.0.0.1:6379> set k1 1			# 整数,类型为"int"
    127.0.0.1:6379> type k1				# 数据类型为 
    string
    127.0.0.1:6379> object encoding k1
    "int"
    127.0.0.1:6379> set k1 a			# 小于44位,类型为"embstr"
    127.0.0.1:6379> object encoding k1
    "embstr"
    127.0.0.1:6379> append k1 b			# 只要值发生改变,即使值没有超过44,编码也会变成"raw"
    (integer) 2
    127.0.0.1:6379> object encoding k1
    "raw"
    127.0.0.1:6379> set k1 aaa...aaa(超过44位)	# 超过44位,类型为"raw"
    127.0.0.1:6379> object encoding k1
    "raw"
    

    总结: Redis String 之所以有 "int"、 "embstr"、 "raw" 三种格式,都是为了节省内存空间。

    1.2.1 SDS 数据结构

    Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 用作 Redis 的默认字符串表示。

    (1)什么是 SDS

    图1 Redis SDS数据结构
    在 3.2 以后的版本中,SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 2^5=32byte,2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB。
    struct __attribute__ ((__packed__)) sdshdr8 {
        uint8_t len;	// 当前字符数组的长度
        uint8_t alloc;	// 当前字符数组总共分配的内存大小
        unsigned char flags; // 当前字符数组的属性、用来标识sdshdr8、sdshdr16
        char buf[];		// 字符串真正的值
    };
    

    (2)为什么要用SDS

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

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

    (3)embstr 和raw 的区别?

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

    因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。

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

    当 int 数据不再是整数, 或大小超过了 long 的范围(2^63-1=9223372036854775807)时,自动转化为 embstr。

    1.3 Redis数据存储结构

    Redis 是 Key-Value 的数据库,它是通过 hashtable 实现的(外层的哈希),其中 value 为 redisObject 结构。

    (1)dict

    dict.h 中定义了 dict 的数据结构。 key 是键的指针, value 是值的指针,value 的类型是 redisObject 。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。next 指向下一个dictEntry。

    图2 Redis的Key-Value数据结构
    **(2)redisObject**

    server.sh 中定义了 redisObject 数据结构。

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

    可以使用 type 命令来查看对外的类型。

    2. Hash

    2.1 基本操作

    hset h1 f 6					# 添加元素
    hmset h1 a 1 b 2 c 3 d 4	# 批量添加元素
    hget h1 a					# 获取元素
    hmget h1 a b c d			# 批量获取元素
    hkeys h1					# 获取 field
    hvals h1					# 获取 value
    hgetall h1					# 获取 field + value
    hget exists h1				# 是否存在
    hdel h1	a					# 删除 field
    hlen h1						# hlen 中 field 个数
    

    2.2 数据结构

    1. ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)。元素个数小于 512 个,且元素值小于 64 字节,使用 ziplist 存储。
    2. hashtable:OBJ_ENCODING_HT(哈希表)。上述条件都不满足时使用 hashtable 存储。

    在 redis.conf 中,可以配置 ziplist 转换为 hashtable 数据结构的阀值:

    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64
    

    2.2.1 ziplist

    压缩列表是 Redis 为了节约内存而开发的,它是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率。 ziplist 时间复杂度是 O(n),适合字段个数少,字段值小的场景。本质上是一种时间换空间的思想。

    图3 ziplist压缩列表结构
    Redis 中 ziplist.h 中定义了 ziplist 的结构。

    (1)ziplist 数据结构

    图 ziplist 数据结构
    ```xml 偏移量(ziplist指针p地址 + zltail = entryN 地址) | ... | | | | 总字节数 entry个数 节点内容 结束标记 ```

    (2)zlentry 数据结构

    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;            // *节点value值
    } zlentry;
    

    zlentry 存储了上一个节点的长度,通过长度查找下一个节点。所以查找的时间复杂度是 O(n),但节省内存。

    2.2.2 hashtable

    Redis 的字典使用哈希表作为底层实现,一个哈希表 dictht 里面可以有多个哈希表节点 dictEntry,而每个哈希表节点就保存了字典中的一个键值对。

    图4 hashtable压缩列表结构
    Redis 中 dict.h 中定义了 hashtable 的结构。dict -> dicht -> dictEntry

    (1)dict

    typedef struct dict {
        dictType *type;		// 类型特定函数
        void *privdata;		// 私有数据
        dictht ht[2];		// *hash表
        long rehashidx; 	// rehash不进行时 rehashidx=-1
        unsigned long iterators; /* number of iterators currently running */
    } dict;
    

    ht[2] 是长度为 2 的 dicht,之所以长度是 2,是为了扩容使用。

    (2)dicht

    typedef struct dictht {
        dictEntry **table;		// *hash数组
        unsigned long size;		// hash数组长度	
        unsigned long sizemask;	// hash表大小掩码,用于计算索引。sizemask=size-1
        unsigned long used;		// hash表中已经使用的数量
    } dictht;
    

    table 属性是一个数组,数组中的每个元素都 dictEntry 结构。dictEntry 用于存储数据。

    (3)dictEntry

    typedef struct dictEntry {
        void *key;				// 键
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;					// 值
        struct dictEntry *next;	// 指向下一个 dictEntry
    } dictEntry;
    

    key 属性保存着键值对中的键,而 v 属性则保存着键值对中的值。其中键值对的值可以是一个指针,或者是一个uint64t 整数,又或者是一个 int64t 整数。

    next 属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一次,以此来解决键冲突(collision)的问题。

    补充问题:hash 扩容,为什么要定义两个哈希表呢?ht[2]

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

    • 比率在 1:1 时(一个哈希表 ht 只存储一个节点 entry),哈希表的性能最好;

    • 如果节点数量比哈希表的大小要大很多的话(这个比例用 ratio 表示,5 表示平均一个 ht 存储 5 个entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在。在这种情况下需要扩容。Redis 里面的这种操作叫做 rehash。

    rehash 的步骤:

    1. 为字符 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对的数量。扩展:ht[1] 的大小为第一个大于等于 ht[0].used * 2。
    2. 将所有的 ht[0] 上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放入指定的位置。
    3. 当 ht[0] 全部迁移到了 ht[1] 之后,释放 ht[0] 的空间,将 ht[1] 设置为 ht[0] 表,并创建新的ht[1],为下次rehash 做准备。

    什么时候触发扩容?

    ratio = used / size,已使用节点与字典大小的比例大于 dict_force_resize_ratio(默认比率是 5) 时,触发扩容。

    3. List

    3.1 基本操作

    lpush q1 a		# 向列表中添加元素
    lpush q1 b c	
    rpush q1 d e	# lpush头,rpush尾
    lpop q1			# 弹出元素
    rpop q1
    lindex q1 0
    lrange q1 0 -1
    

    3.2 数据结构

    在 3.0 之前,Redis 使用 ziplist 和 linkedlist 数据结构。在之后使用 quicklist 数据结构。

    3.2.1 quicklist

    quicklist 是双向链表结构,每个节点实际存储的是 ziplist 数据结构。

    图5 quicklist快速列表结构
    **(1)quicklist**
    typedef struct quicklist {
        quicklistNode *head;		// *链表头节点
        quicklistNode *tail;		// *链表尾节点
        unsigned long count;        // 元素总个数=所有ziplists元素个数总和
        unsigned long len;          // quicklistNodes 个数
        int fill : 16;              // fill factor for individual nodes
        unsigned int compress : 16; // depth of end nodes not to compress;0=off
    } quicklist;
    

    quicklist 是一个双向链表,每个节点是 quicklistNode。

    (2)quicklistNode

    typedef struct quicklistNode {
        struct quicklistNode *prev;
        struct quicklistNode *next;
        unsigned char *zl;			 // *ziplist
        unsigned int sz;             // 单个节点总字节数
        unsigned int count : 16;     // 单个节点中元素个数
        unsigned int encoding : 2;   // 编码方式:RAW==1 or LZF==2
        unsigned int container : 2;  // *内部数据节点,默认ziplist:NONE==1 or ZIPLIST==2
        unsigned int recompress : 1; // was this node previous compressed? */
        unsigned int attempted_compress : 1; // node can't compress; too small */
        unsigned int extra : 10; // more bits to steal for future usage */
    } quicklistNode;
    

    quicklistNode 内部默认是 ziplist。

    4. Set

    4.1 基本操作

    sadd s1 a b c d e f g
    smembers s1				# 集合中所有元素
    scard s1				# 集合中元素个数
    srandmember s1			# 随机获取一个元素
    spop s1					# 弹出并删除元素
    srem s1 d e f			# 删除元素
    sismember s1 a			# 判断一个元素是否是集合成员
    
    sdiff set1 set2			# 获取差集
    sinter set1 set2		# 获取交集(intersection )
    sunion set1 set2		# 获取并集
    

    4.2 数据结构

    1. intset:集合中元素全部是整数,并且元素个数小于 512 个,使用 intset 存储。
    2. hashtable:集合中元素只要不是整数,使用 hashtable 存储。

    数据结构示例:

    192.168.139.101:6379> sadd s1 1 2 3 4
    192.168.139.101:6379> object encoding s1
    "intset"
    192.168.139.101:6379> sadd s1 a
    192.168.139.101:6379> object encoding s1
    "hashtable"
    

    总结: 当集合 s1 中元素全部是整数时,数据类型为 "intset",当添加非整数元素后,数据类型为 "hashtable"。

    4.2.1 intset

    typedef struct intset {
        uint32_t encoding;	// 编码方式
        uint32_t length;	// 集合中包含的元素个数
        int8_t contents[];	// 保存元素的数组
    } intset;
    

    contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。

    4.2.2 hashtable

    5. Sorted Set

    5.1 基本操作

    zadd z1 10 java 20 php 30 ruby 40 cpp 50 python	# 添加元素(score element)
    zrange z1 0 -1 withscores		# 获取元素
    zrevrange z1 0 -1 withscores	# 倒序获取元素
    zrangebyscore z1 20 30			# score在20~30的元素
    zrem z1 php cpp					# 删除元素
    zcard z1						# 元素个数
    zincrby z1 5 python				# 修改元素score
    zcount z1 20 60					# 指定score范围的个数
    zrank z1 java
    zscore z1 java
    

    5.2 数据结构

    1. ziplist:元素个数小于 128 时,且元素的值大小小于 64,数据结构为 ziplist。
    2. skiplist + dict:上述两个条件,任何一个不满足时,都会转换成 跳表 + dict 结构。

    在 redis.conf 中,可以配置 ziplist 转换为 skiplist + dict 数据结构的阀值:

    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64
    

    5.2.1 skiplist

    我们知道有序数组可以通过二分法查找元素,时间复杂度为 O(log2n)。但如果是一个有序的链表呢,能不能也通过二分法快速查找元素呢?一个办法是给链表增加指针,level 是随机的。有序链表结构和跳表结构如下:

    图6 有序链表结构
    图7 跳表skiplist结构
    **总结:** 跳表设置 level 后,查找方式也是类似二分法查找。在这个查找过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了。需要比较的节点数大概只有原来的一半。这就是 **跳跃表。**

    为什么不用 AVL 树或者红黑树?因为 skiplist 更加简洁。

    (1)zskiplist

    在 server.h 定义了 zskiplist 结构

    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;
    
    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    

    (2)随机获取层数的函数
    源码:t_zset.c

    int zslRandomLevel(void) {
        int level = 1;
        while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
        return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    }
    

    6. hyperloglogs

    数据统计。

    7. geospatial

    地理位置。

    8. 总结

    表1 数据结构总结
    | 对象 | 对象type属性值 | type 命令输出 | 底层可能的存储结构 | object encoding | | ------------ | -------------- | ------------- | ------------------------------------------------------------ | ------------------------------ | | 字符串对象 | OBJ_STRING | "string" | OBJ_ENCODING_INT
    OBJ_ENCODING_EMBSTR
    OBJ_ENCODING_RAW | int
    embstr
    raw | | 列表对象 | OBJ_LIST | "list" | OBJ_ENCODING_QUICKLIST | quicklist | | 哈希对象 | OBJ_HASH | "hash" | OBJ_ENCODING_ZIPLIST
    OBJ_ENCODING_HT | ziplist
    hashtable | | 集合对象 | OBJ_SET | "set" | OBJ_ENCODING_INTSET
    OBJ_ENCODING_HT | intset
    hashtable | | 有序集合对象 | OBJ_ZSET | "zset" | OBJ_ENCODING_ZIPLIST
    OBJ_ENCODING_SKIPLIST | ziplist
    skiplist(包含ht) |
    表2 编码转换总结
    | 对象 | 原始编码 | 升级编码 | | | ------------ | ------------------------------------------------------------ | ------------------------------------- | ---- | | 字符串对象 | INT
    整数并且小于long 2^63-1 | embstr
    超过44 字节,被修改 | raw | | 哈希对象 | ziplist
    键和值的长度小于64byte,键值对个数不
    超过512 个,同时满足 | hashtable
    整数并且小于long 2^63-1 | | | 列表对象 | quicklist | hashtable | | | 集合对象 | intset
    元素都是整数类型,元素个数小于512 个,
    同时满足 | | | | 有序集合对象 | ziplist
    元素数量不超过128 个,任何一个member
    的长度小于64 字节,同时满足。 | skiplist | |

    每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 相关阅读:
    如何实现分页功能
    学习Python的心路历程
    Python基础---协程
    Python基础---线程
    Python基础---python中的进程操作
    Python基础---进程相关基础
    Python基础---并发编程(操作系统的发展史)
    Python基础---网络编程3
    Python基础---网络编程2
    Python基础---面向对象3
  • 原文地址:https://www.cnblogs.com/binarylei/p/11717598.html
Copyright © 2011-2022 走看看