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


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

Redis 系列(02)数据结构的更多相关文章

  1. Redis系列二 - 数据结构

    前言 redis作为我们开发的一大神器,我们接触肯定不会少,但是很多同学也许只会存储String类型的值,这是非常不合理的.在这里,将带大家认识Redis的5中数据结构. 1.问:Redis有那些数据 ...

  2. redis 系列7 数据结构之跳跃表

    一.概述 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.在大部分情况下,跳跃表的效率可以和平衡树(关系型数据库的索引就是平衡树 ...

  3. redis 系列8 数据结构之整数集合

    一.概述 整数集合(intset)是集合键的底层实现之一, 当一个集合只包含整数值元素,并且这个集合元素数量不多时, Redis就会使用整数集合作为集合键的底层实现.下面创建一个只包含5个元素的集合键 ...

  4. redis 系列6 数据结构之字典(下)

    一.概述 接着上篇继续,这篇把数据结构之字典学习完, 这篇知识点包括:哈希算法,解决键冲突, rehash , 渐进式rehash,字典API. 1.1 哈希算法 当一个新的键值对 需要添加到字典里面 ...

  5. redis 系列5 数据结构之字典(上)

    一. 概述 字典又称符号表(symbol table),关联数组(associative array), 映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构.在字典中, ...

  6. redis 系列4 数据结构之链表

    一. 概述 链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可能通过增删节点来灵活地调整链表的长度.作为一种数据结构,在C语言中并没有内置的这种数据结构.所以Redis构建了自己的链表实现 ...

  7. redis 系列3 数据结构之简单动态字符串 SDS

    一.  SDS概述 Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默 ...

  8. redis 系列14 有序集合对象

    一. 有序集合概述 Redis 有序集合对象和集合对象一样也是string类型元素的集合,且不允许重复的成员.不同的是每个元素都会关联一个double类型的分数.redis正是通过分数来为集合中的成员 ...

  9. 【目录】redis 系列篇

    随笔分类 - redis 系列篇 redis 系列27 Cluster高可用 (2) 摘要: 一. ASK错误 集群上篇最后讲到,对于重新分片由redis-trib负责执行,关于该工具以后再介绍.在进 ...

随机推荐

  1. hdu6311 Cover (欧拉路径输出)

    hdu6311Cover 题目传送门 题意:有最少用多少条边不重复的路径可以覆盖一个张无向图. 分析:对于一个连通块(单个点除外),如果奇度数点个数为 k,那么至少需要max{k/2,1}  条路径. ...

  2. Android APP 登陆模块

    首先我想强调一点.这个登陆的模块最好是放在另外一个线程里面来实现.否则有可能会爆出一系列的问题, 然后再与主UI 交互.这样就不会爆ANR异常 1.对于登陆模块的.首先大体的逻辑肯定是要清晰的.    ...

  3. This program cannot be run in DOS mode.

    问题:通过ftp上传的exe执行时提示“This program cannot be run in DOS mode.” 解决方法:检查ftp传输模式,设置成binary模式上传即可 参考:https ...

  4. 在CNN上增加一层CAM告诉你CNN到底关注什么

    Cam(Class Activation Mapping)是一个很有意思的算法,他能够将神经网络到底在关注什么可视化的表现出来.但同时它的实现却又如此简介,相比NIN,googLenet这些使用GAP ...

  5. 没有dockerfile的情况下如何查看docker的镜像信息

    前言 参考资料 https://baijiahao.baidu.com/s?id=1564406878758073&wfr=spider&for=pc 很实用的功能哈.. 步骤 1.先 ...

  6. mysql数据库进阶

    一.索引 索引,是数据库中专门用于帮助用户快速查询数据的一种数据结构.类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,然后直接获取即可. 分类: 普通索引 唯一索引 全文索引 组合 ...

  7. 【串线篇】数据库设计之加谈n-n

    // n-n即(1-n)+(n-1): // 1-n:n-1:n-n:外键应该放在哪个表? //结论: //一对n:外键一定放在n的一端:别让一个人去记14亿,而让大家只记一个习大大 //n-n:”中 ...

  8. 第01章 Spring概述

    第01章 Spring概述 1.Spring概述 ①Spring是一个开源框架 ②Spring为简化企业级开发而生,使用Spring,JavaBean就可以实现很多以前要靠EJB才能实现的功能.同样的 ...

  9. AGC020C Median Sum

    高端操作qaq 又双叒叕读错题了= = 然后重新读题发现不会做了 于是瞅了一波题解 我靠要不要这么暴力呜呜呜 直接bitset O(n^3/w)QAQ 就是f[i]表示i是否能被搞出来 然后我们先不看 ...

  10. Redis缓存在django中的配置

    django  settings中的配置 # 缓存 CACHES = { "default": { "BACKEND": "django_redis. ...