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 字符串类型的内部编码有三种:
- int,存储8个字节的长整型(long,2^63-1)。
- embstr SDS(Simple Dynamic String),存储小于44 个字节的字符串。。
- 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
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 当前字符数组的长度
uint8_t alloc; // 当前字符数组总共分配的内存大小
unsigned char flags; // 当前字符数组的属性、用来标识sdshdr8、sdshdr16
char buf[]; // 字符串真正的值
};
(2)为什么要用SDS
我们知道,C 语言本身没有字符串类型(只能用字符数组char[]实现)。
- 不用担心内存溢出问题,如果需要会对SDS 进行扩容。
- 获取字符串长度时间复杂度为O(1),因为定义了len 属性。
- 通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。
- 判断是否结束的标志是 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。
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 数据结构
ziplist
:OBJ_ENCODING_ZIPLIST(压缩列表)。元素个数小于 512 个,且元素值小于 64 字节,使用 ziplist 存储。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),适合字段个数少,字段值小的场景。本质上是一种时间换空间的思想。
(1)ziplist 数据结构
(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,而每个哈希表节点就保存了字典中的一个键值对。
(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 的步骤:
- 为字符 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对的数量。扩展:ht[1] 的大小为第一个大于等于 ht[0].used * 2。
- 将所有的 ht[0] 上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放入指定的位置。
- 当 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 数据结构。
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 数据结构
intset
:集合中元素全部是整数,并且元素个数小于 512 个,使用 intset 存储。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 数据结构
ziplist
:元素个数小于 128 时,且元素的值大小小于 64,数据结构为 ziplist。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 是随机的。有序链表结构和跳表结构如下:
为什么不用 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. 总结
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) |
整数并且小于long 2^63-1 | embstr
超过44 字节,被修改 | raw | | 哈希对象 | ziplist
键和值的长度小于64byte,键值对个数不
超过512 个,同时满足 | hashtable
整数并且小于long 2^63-1 | | | 列表对象 | quicklist | hashtable | | | 集合对象 | intset
元素都是整数类型,元素个数小于512 个,
同时满足 | | | | 有序集合对象 | ziplist
元素数量不超过128 个,任何一个member
的长度小于64 字节,同时满足。 | skiplist | |
每天用心记录一点点。内容也许不重要,但习惯很重要!