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
图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[]实现)。
- 不用担心内存溢出问题,如果需要会对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。
图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 数据结构
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),适合字段个数少,字段值小的场景。本质上是一种时间换空间的思想。
图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 的步骤:
- 为字符 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 数据结构。
图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 数据结构
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 是随机的。有序链表结构和跳表结构如下:
图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)数据结构的更多相关文章
- Redis系列二 - 数据结构
前言 redis作为我们开发的一大神器,我们接触肯定不会少,但是很多同学也许只会存储String类型的值,这是非常不合理的.在这里,将带大家认识Redis的5中数据结构. 1.问:Redis有那些数据 ...
- redis 系列7 数据结构之跳跃表
一.概述 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.在大部分情况下,跳跃表的效率可以和平衡树(关系型数据库的索引就是平衡树 ...
- redis 系列8 数据结构之整数集合
一.概述 整数集合(intset)是集合键的底层实现之一, 当一个集合只包含整数值元素,并且这个集合元素数量不多时, Redis就会使用整数集合作为集合键的底层实现.下面创建一个只包含5个元素的集合键 ...
- redis 系列6 数据结构之字典(下)
一.概述 接着上篇继续,这篇把数据结构之字典学习完, 这篇知识点包括:哈希算法,解决键冲突, rehash , 渐进式rehash,字典API. 1.1 哈希算法 当一个新的键值对 需要添加到字典里面 ...
- redis 系列5 数据结构之字典(上)
一. 概述 字典又称符号表(symbol table),关联数组(associative array), 映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构.在字典中, ...
- redis 系列4 数据结构之链表
一. 概述 链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可能通过增删节点来灵活地调整链表的长度.作为一种数据结构,在C语言中并没有内置的这种数据结构.所以Redis构建了自己的链表实现 ...
- redis 系列3 数据结构之简单动态字符串 SDS
一. SDS概述 Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默 ...
- redis 系列14 有序集合对象
一. 有序集合概述 Redis 有序集合对象和集合对象一样也是string类型元素的集合,且不允许重复的成员.不同的是每个元素都会关联一个double类型的分数.redis正是通过分数来为集合中的成员 ...
- 【目录】redis 系列篇
随笔分类 - redis 系列篇 redis 系列27 Cluster高可用 (2) 摘要: 一. ASK错误 集群上篇最后讲到,对于重新分片由redis-trib负责执行,关于该工具以后再介绍.在进 ...
随机推荐
- Codeforces - 1189B - Number Circle - 贪心
https://codeforc.es/contest/1189/problem/B 优先考虑最大的元素怎么构造.拿两个次大的围着他就很好,但是其他的怎么安排呢?就直接降序排列就可以了. a数组还开错 ...
- 1233: [Usaco2009Open]干草堆tower
传送门 感觉正着做不太好搞,考虑倒过来搞 容易想到贪心,每一层都贪心地选最小的宽度,然后发现 $WA$ 了... 因为一开始多选一点有时可以让下一层宽度更小 然后有一个神奇的结论,最高的方案一定有一种 ...
- Android APP 登陆模块
首先我想强调一点.这个登陆的模块最好是放在另外一个线程里面来实现.否则有可能会爆出一系列的问题, 然后再与主UI 交互.这样就不会爆ANR异常 1.对于登陆模块的.首先大体的逻辑肯定是要清晰的. ...
- Linux scp常用命令
Linux scp命令用于Linux之间复制文件和目录. scp是 secure copy的缩写, scp是linux系统下基于ssh登陆进行安全的远程文件拷贝命令. 1.从本地复制到远程 命令格式: ...
- datagridview里面的checkbox全选和取消全选
全选 设置全选button,选中所有的checkbox private void selectAll_Click(object sender, EventArgs e) { //遍历datagridv ...
- JS中的匿名函数、回调函数、匿名回调函数
工欲善其事必先利其器 在学习JavaScript设计模式一书时,遇到了“匿名回调函数”这个概念,有点疑惑,查找了些资料重新看了下函数的相关知识点之后,对这个概念有了认识.九层之台,起于垒土.在熟悉这一 ...
- 负载均衡算法WeightedRoundRobin(加权轮询)简介及算法实现
Nginx的负载均衡默认算法是加权轮询算法,本文简单介绍算法的逻辑,并给出算法的Java实现版本. 本文参考了Nginx的负载均衡 - 加权轮询 (Weighted Round Robin). ...
- mpvue 微信小程序半屏弹框(half-screen-dialog)
<template> <div> <a @click="isShow">half-screen-dialog</a> <!-- ...
- socket - Linux 套接字
总览 #include <sys/socket.h> mysocket = socket(int socket_family, int socket_type, int protocol) ...
- python列表解析和生成器表达式
列表解析作为动态创建列表的强大工具,值得学习. 列表解析技术之前的状况--函数式编程. lambda.filter(), map() enumerate, sorted, any, all, zip ...