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 属性(它同样以'\0'结尾是因为这样就可以使用 C 语言中函数库操作字符串的函数了),可以包含'\0'。

(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 | |

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

posted on 2019-10-22 20:59  binarylei  阅读(395)  评论(0编辑  收藏  举报

导航