Redis 学习总结—数据结构

Redis 一款开源、高性能的 key-value型数据存储系统,工作在内存中,支持多种类型的数据结构,可以用做数据库,缓存和消息中间件。支持数据的持久化、备份复制等,通过Redis Sentinel和Cluster等提高系统的高可用。

Redis 数据类型

Redis 除了常用的数据类型string(字符串),list(列表),hash(散列表),set(集合),sort set(有序集合)外,还支持 bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询等

String 字符串

string 类型在存储整数时,内部存储使用int编码方式;当存储的数据是字符串时,string内部则使用SDS简单动态字符串结构来存储。一个key值最大能存储 512MB。

SDS 数据结构

一个SDS数据结构,主要有len、free、buf[]三个属性组成

struct sdshdr{
  int free; //buf[]数组中未使用的字节的数量
  int len; //buf[]数组中所保存的字符串的长度
  char buf[]; //保存字符串的数组,redis会在数组最后加一个’\0‘表示字符串的结束,会额外占用1个字节的内存
}

在SDS中,除了buf[]固定占用的1个字节外,len和free也会分别占用4个字节存储对应数据,这些是SDS结构体的额外开销。
除了上述额外开销外,string还有一个来自redisObject结构体的开销。因为Redis的多种数据结构,不同的数据类型都有相同的元数据要记录(最后一次的访问时间、被引用的次数等),所以Redis用一个redisObject结构体存储这些元数据,同时有指针指向实际数据。
一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针,这个指针再进一步指向具体数据类型的实际数据所在,例如指向 String 类型的 SDS 结构所在的内存地址。

为了节省内存空间,Redis 还对 SDS 的内存布局做了专门的设计。内部使用三种编码方式存储数据,分别是int编码格式,embstr编码格式,raw编码格式。
1,当保存的是数值型数据时, 结构体内部使用long 类型来存储数据,而RedisObject 中的指针就直接赋值为整数数据,这样就不用额外的指针再指向整数了,节省了指针的空间开销。
2,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域(申请1次空间),这样就可以避免内存碎片。这种布局方式被称为 embstr 编码方式。
3,当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间(需要申请2次空间),并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
注意
当我们对一个embstr编码的key进行append操作时,由于当前key是只读的(因为RedisObject 和 SDS内存是在一起的,如果要进行修改时则需要重新申请2次内存空间,为了避免此种情况,所以禁止embstr编码方式的字符串进行修改操作),所以SDS内部会将其修改成raw编码格式后再对其进行添加操作。
编码的修改升级不可逆,即便是后续再次修改的key值大小符合原来的编码格式,编码方式也不会再发生改变。

SDS结构相较于C中字符串结构的优势:
1,高效执行长度计算O(1)(可以直接通过len值获取)。
2,高效执行append操作(通过free根据阈值判断提前分配翻倍叠加的空间大小)。
3,SDS提供空间预分配惰性空间释放两种内存分配策略,减少连续的执行字符串增长带来内存重新分配的次数。
4,二进制安全,以二进制的方式处理数据,数据是怎样存储的,取出来就是什么样。

内存重分配

SDS通过两种内存重分配策略,解决字符串在增长和缩短时的内存分配问题
1,空间预分配
当redis在修改字符串并需对SDS的空间进行扩展时,不仅会为SDS分配修改所必要的空间,还会为SDS分配额外的未使用空间free,下次再修改就先检查未使用空间free是否满足,满足则不用在扩展空间。
额外分配未使用空间free的规则:
如果对 SDS 字符串修改后,len 值小于 1M,那么此时额外分配未使用空间 free 的大小与len相等(增加原来len 1倍的空间)。
如果对 SDS 字符串修改后,len 值大于等于 1M,那么此时额外分配未使用空间 free 的大小为1M(增加1M的空间)。

2,惰性空间释放
当缩短SDS字符串后,并不会立即执行内存重分配来回收多余的空间,而是用free属性将这些空间记录下来,如果后续有增长操作,则可直接使用。

String常用操作命令

SET key value  #设置key的值为value
GET key value  #获取key的值value
APPEND key value  #将指定的value追加到该key原来值的末尾。
SETEX key seconds value  #设置key的值为value,并设置过期时间为seconds(单位是秒)
SETNX key value  #只有在key不存在时设置key的值为value
INCR key  #将key的值加1,自加命令
DECR key  #将key的值减1,自减命令
INCRBY key amount  #将key的值加整数amount
DECRBY key amount  #将key的值减整数amount
STRLEN key  #返回key所储存的字符串值的长度
SETRANGE key offset value  #用value参数覆写给定key所储存的字符串值,从偏移量offset开始。

场景应用

1,计数器/粉丝数,使用INCR key命令实现数据的自增操作
2,字符串存储,图片使用bit方式存储
3,缓存存储,热点数据
4,set key value NX EX 实现分布式锁 或者使用命令setnx key value代替
5,分布式session存储

相关问题解答

1,为什么 Redis 对字符串进行优化而不是直接使用char[]进行存储?
因为 redis 是一个 key-value 类型的数据库,key 是字符串,但 value 可以是集合,列表,hash 等,同时 redis 会进行各种操作,频繁但使用长度和 append 操作,而 char[] 的长度操作是O(n),append 会进行N次 realloc,另外 redis 分为客户端和服务端,之间传递的内容必须是二进制安全的,而 char[] 类型则是必须保证以\0结尾,而其他字符可能是正常的字符串,所以在这两个基础上开发了SDS。

List 列表

列表类型List可以存储一个有序的字符串列表,常用的操作是在列表两端添加元素,或者取列表的某一段。一个列表最多可以包含 232 - 1 个元素。
在 redis 3.2版本之前List底层结构实现是压缩列表 (ziplist) + 双向琏表 (linkedlist),默认情况下使用 ziplist 结构存储数据,在下列情况下,底层则会转化成 linkedlist 结构。在3.2版本之后引入了 quicklist,quicklist 是对链表和压缩列表进行改造的一种数据结构,是 zipList 和 linkedList 的混合体。

ziplist 压缩列表结构

压缩列表就是由一系列特殊编码的内存块构成的列表,一个压缩列表可以包含多个节点,每个节点可以保存一个长度受限的字符数组或整数,以最小内存块进行扩展。
ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一 个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能, 来换取高效的内存空间利用率,是一种时间换空间的思想。
Redis使用字节数组表示一个压缩列表,字节数组逻辑划分为多个字段。
在这里插入图片描述
其中
zltail:占用个字节,记录尾节点到head的偏移量(到起始地址有多少字节);
zllen:占用2个字节,记录节点的个数;
zlbytes:占用4个字节,记录整个压缩列表占用的内存字节数;
entryX:压缩列表存储的若干个元素(实际的data结构体),可以是字符串数组或整数;
zlend:恒为0xFF,占用1个字节,表示压缩列表的结尾。

entry结构体
压缩列表中每个entry结构体又有三部分组成。
在这里插入图片描述
previous_entry_length:表示前一个元素的字节长度,占1个或者5个字节;当前一个元素的长度小于254字节时,previous_entry_length字段用一个字节表示;当前一个元素的长度大于等于254字节时,previous_entry_length字段用5个字节来表示,而这时previous_entry_length的第一个字节是固定的标志0xFE,后面4个字节才真正表示前一个元素的长度。已知当前元素的首地址为p,那么(p-previous_entry_length)就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历;
encoding:表示当前元素的编码,即content字段存储的数据类型(整数或者字节数组),为了节约内存,使用的也是可变长度编码
content:存放数据的内容

encoding编码encoding长度content类型
00 bbbbbb(6比特表示content长度)1字节最大长度63的字节数组
01 bbbbbb xxxxxxxx(14比特表示content长度)2字节最大长度(2^14)-1字节最大长度的字节数组
10__ aaaaaaaa bbbbbbbb cccccccc dddddddd(32比特表示content长度)5字节最大长度(2^32)-1的字节数组;10后面有6个bit位设为0,暂时不用
11 00 00001字节int16整数
11 01 00001字节int32整数
11 10 00001字节int64整数
11 11 00001字节24比特整数
11 11 11101字节8比特整数
11 11 xxxx1字节没有content字段;xxxx表示0~12之间的整数

不同编码的内存长度通过encoding + length 搞定,encoding占前两位,00,01,10,11表示不同的类型,其中11表示节点存放的是整型,其他都是不同长度的字符串(字节数组)。

typedef struct zlentry {
    unsigned int prevrawlensize; //表示previous_entry_length字段的长度
    unsigned int prevrawlen; //表示previous_entry_length存储的内容
     
    unsigned int lensize; //表示encoding字段的长度                                
    unsigned int len; //表示数据内容的长度
            
    unsigned int headersize; //表示当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和。
    
    unsigned char encoding; //表示数据类型
      
    unsigned char *p; //当前元素的首地址          
} zlentry;

ziplist 的优势:
1,压缩列表是一种为节约内存而开发的顺序型数据结构。
2,压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。单个字节数组最多可存储 232 - 1 个字节。

linkedlist 双向无环琏表

linkedlist 的底层是琏表实现的,但由于C中没有内置的琏表结构,所以Redis实现了自己的琏表结构。

linkedlist 是双向琏表,需要额外的两个空间来存储后继结点和前驱结点的地址,在数据量较小的情况下会造成空间上的浪费(因为数据量小的时候速度上的差别不大,但空间上的差别很大)。所以Redis在列表对象中数据量较小的时候使用压缩列表作为底层实现,而数据量大的时候才会使用双向无环链表。
数据量大小的限定条件:

1,当向列表中插入一个字符串,字符串长度大于server.lisr_max_ziplist_value(默认是64)时
2,当压缩列表ziplist中包含的节点超过server.lisr_max_ziplist_entries(默认是512)时

linkedlist 有 listNode 和 list 两个结构体组成,其中listNode是琏表的节点,包含前驱指针prev,后继指针next和值value;list是双向琏表本身,包含表头指针head,表尾指针tail,节点数量len,复制函数dup,释放(删除)函数free,对比函数match。

typedef struct listNode{
   struct listNode *prev; //节点的前驱指针
   struct listNode *next; //节点的后驱指针
   void *value;	//节点的值
}listNode;

typedef struct list{
   listNode *head; //表头指针,前驱指针指向null
   listNode *tail; //表尾指针,后驱指针指向null
   unsigned long len; //记录节点listNode的数量
   void *(*duo)(void *ptr); //用于复制链表节点所保存的值
   void *(*free)(void *ptr); //用于释放链表节点所保存的值
   int (*match)(void *ptr,void *key); //用于对比链表节点所保存的值和另一个输入值是否相等
}list;

在这里插入图片描述

Redis中双向链表的特性:
1,琏表的每个节点都有指向前一个节点和后一个节点的指针。
2,链表是无环的,表头节点和表尾节点的prev和next指针指向null。
3,链表有自己长度的信息,获取长度的时间复杂度为O(1)。
4,带有表头指针,表尾指针,获取表头和表尾节点的时间复杂度都为O(1),LPUSH、RPOP、RPUSH、LPOP命令实现的关键。

quicklist 快速列表

由于 linkedlist 的附加空间相对太高,prev 和 next 指针就要占用 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。因此Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。它将 linkedList 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。

typedef struct quicklistNode {
    struct quicklistNode *prev; //当前节点的前驱指针,上一个节点的地址
    struct quicklistNode *next; //当前节点的后驱指针,下一个节点的地址
    unsigned char *zl; //数据指针。如果当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。
    unsigned int sz; //表示zl指向的ziplist的总大小(包括zlbytes, zltail, zllen, zlend和各个数据项),如果进行了压缩,记录的也是压缩前的大小      
    unsigned int count : 16; //ziplist里面包含的数据项个数,该字段只有16bit  
    unsigned int encoding : 2; //表示ziplist是否压缩,2表示使用了LZF算法压缩,1表示没有压缩
    unsigned int container : 2; //预留字段,用作数据容器,目前固定值为2,表示使用ziplist作为数据容器
    unsigned int recompress : 1; //对一个压缩的数据暂时解压查看时作的标记,后续需要重新压缩
    unsigned int attempted_compress : 1; //只对Redis的自动化测试程序有用
    unsigned int extra : 10; //其他扩展字段,暂时无用
} quicklistNode;  //quicklist的一个节点

typedef struct quicklistLZF {
    unsigned int sz; //压缩后的ziplist大小
    char compressed[]; //用来存放压缩后的字节数组
} quicklistLZF;  //一个被压缩过的ziplist

typedef struct quicklist {
    quicklistNode *head; //表头指针,quicklist第一个节点地址
    quicklistNode *tail; //表尾指针,quicklist最后一个节点的地址
    unsigned long count;  //所有节点中的ziplist数据项的个数总和
    unsigned int len; //quicklist节点的个数    
    int fill : 16; //ziplist的大小设置,占用16个字节,存放list-max-ziplist-size参数的值         
    unsigned int compress : 16; //节点压缩深度设置,占用16个字节,存放list-compress-depth参数的值
} quicklist;  //quicklist列表本身

在这里插入图片描述

配置参数解析
list-max-ziplist-size:用来设置 quicklist 节点中一个 ziplist 存放的数据量的大小。可以取正值,也可以取负值。
当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。
当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时只能取-1到-5这五个值,每个值含义如下:

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

list-compress-depth:用来设置 quicklist 两端节点不需要被压缩的个数。将 quicklist 中间的数据节点进行压缩,从而进一步节省内存空间。取值含义如下:

0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
依此类推……

List 常用操作命令

LLEN key  #获取列表长度
LSET key index value  #通过索引设置列表元素的值
LPUSH key value1 [value2]  #将一个或多个值添加到列表头部
RPUSH key value1 [value2]  #在列表中添加一个或多个值
LPOP key  #移出并获取列表的第一个元素
BLPOP key1 [key2 ] timeout  #移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOP key1 [key2 ] timeout  #移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

在执行BLPOP/BRPOP/BRPOPLPUSH时,如果列表为空时会造成客户端阻塞。
客户端阻塞的解决方法:
1,被动脱离,有其他客户端为阻塞的 key 加入了元素;
2,主动脱离,超过阻塞的最长等待时间;
3,强制脱离,关闭客户端或服务器。

场景应用

1,阻塞队列,结合lpush和brpop命令实现,生产者使用lupsh从列表的左侧插入元素,消费者使用brpop命令从队列的右侧获取元素进行消费。
2, 消息排序功能,如twitter关注列表,粉丝列表等

相关问题解答

1,redis为什么要有双向琏表?
因为双向琏表是一种通用的数据结构,在redis中内部使用非常多,它即作为redis列表结构的底层实现之一(另一个是压缩列表),又在事务,服务端存储客户端信息,订阅发布模块中保存订阅模式的多个客户端,时间模块来存储时间事件,但C语言没有琏表类型所以redis自己实现了双向琏表结构。

Hash 散列表

Redis hash数据类型 是一个 string 类型的 field(字段) 和 value(值) 的映射表,特别适合存储对象。不光是数据结构,Redis 本身就是一个大的hash,平常操作的key就是hash的键,value就是hash的值。Redis 中每个 hash 可以存储 232 - 1 个键值对(40多亿)。
Hash内部的编码有两个,ziplist 和 hashtable,当存储的数据量比较少时,内部使用 ziplist 存储数据,达到一定数据量后,内部才使用hashTable 编码格式保存数据。

1,哈希表中某个键或值的长度大于server.hash_max_ziplist_value(默认是64)时
2,ziplist中包含的节点数量大于server.hash_max_ziplist_entries(默认是512)时

hashtable 哈希表

Redis中的key-value是通过dictEntry对象来实现的,而哈希表 dictht 就是将dictEntry对象进行了再一次的包装得到的。
Redis就是一个键值对数据库,数据库中的键值对就是由字典保存:每个数据库都有一个与之相对应的字典,这个字典被称为键空间(key space)。

typedef struct dict {
    dictType *type; //字典类型的一些特定函数
    void *privdata; //私有数据,type中的特定函数可能需要用到
    dictht ht[2]; //一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下只用ht[0],ht[1]再rehash时使用
    long rehashidx; //rehash的索引,没有rehash时,值为-1
    unsigned long iterators; //正在使用的迭代器数量
} dict;

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key); //计算哈希值函数
    void *(*keyDup)(void *privdata, const void *key); //复制键函数
    void *(*valDup)(void *privdata, const void *obj); //复制值函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); //对比键函数
    void (*keyDestructor)(void *privdata, void *key); //销毁键函数
    void (*valDestructor)(void *privdata, void *obj); //销毁值函数
} dictType;  //每个dictType用于操作特定类型键值对的函数

typedef struct dictht {
    dictEntry **table; //哈希表数组,其中的每个元素都是一个指向dictEntry的指针
    unsigned long size; //哈希表大小,也是table数组的大小
    unsigned long sizemask; //大小掩码,用于计算索引值,决定一个键应该放到table数组的哪个索引上,总是等于size-1
    unsigned long used; //哈希表中的已有节点数(键值对的数量)
} dictht;

typedef struct dictEntry
{
    void *key; //键
    union{  //值,可以是一个指针,或uint64_t整数,或int64_t整数
      void *val;
       uint64_tu64;
       int64_ts64;
       }v;
    struct dictEntry *next; // 指向下个哈希表节点的指针,可以将多个哈希值相同的键值对连接在一起,形成单向链表,解决键冲突问题。
}dictEntry; //哈希表的节点定义,每个dictEntry结构都保存着一个键值对

hash表内部的dictht使用链地址法来处理键碰撞(链地址法:将全部具有同样哈希地址的而不同keyword的数据元素连接到同一个单链表中。假设选定的哈希表长度为m,则可将哈希表定义为一个有m个头指针组成的指针数组T[0…m-1]。凡是哈希地址为i的数据元素,均以节点的形式插入到T[i]为头指针的单链表中。而且新的元素插入到链表的前端,这不仅由于方便。还由于常常发生这种事实:新近插入的元素最有可能不久又被访问)。

对 Hash 进行迭代实际上就是对 dict 内部的哈希表 ht[2] 进行迭代,迭代器首先迭代 dict 的第一个哈希表 ht[0],然后,如果rehash正在进行的话,就继续对第二个哈希表 ht[1] 进行迭代。当迭代哈希表时,找到第一个不为空的索引,然后迭代这个索引上的所有节点,当这个索引迭代完了,继续查找下一个不为空的索引,如此循环,一直到整个哈希表都迭代完为止。

rehash 重新散列

当设置一个哈希对象时,具体会落到哈希数组(dictEntry*[])中的哪个下标,是通过计算哈希值来确定的,如果发生哈希碰撞,那么同一个下标就会有多个dictEntry,从而形成一个链表(最后插入的总是落在链表的最前面),链表越长,性能越差。所以为了保证哈希表的性能,在满足以下两个条件时,Redis 就会对哈希表进行rehash(重新散列)操作。

扩展条件
1、自然rehash:负载因子大于等于1且dict_can_resize设置为1时(没有执行BGSAVE或BGREWRITEAOF命令时的条件)
2、强制rehash:负载因子大于等于安全阈值dict_force_resize_ratio=5时(正在执行BGSAVE或BGREWRITEAOF命令时的条件)
收缩条件
1,当字典的填充率低于10% (负载因子小于0.1)时

负载因子=哈希表已使用节点数/哈希表大小(即:h[0].used/h[0].size)
1,当字典用于实现哈希键的时候,每次从字典中删除一个键值对,程序就会执行一次htNeedsResize函数,如果字典达到了收缩的标准,程序将立即对字典进行收缩
2,当字典用于实现数据库键空间(key space)的时候, 收缩的时机由redis.c/tryResizeHashTables 函数决定

扩展哈希和收缩哈希都是通过执行rehash来完成,主要经过以下五个步骤:
1、为字典dict的ht[1]哈希表分配空间,其大小取决于当前哈希表已保存节点数(即:ht[0].used)。
1》、扩展操作是自动触发的,ht[1]的大小为 第一个大于等于ht[0]中已经保存的节点数2倍的2的n次幂整数。
2》、收缩操作则是由程序手动执行,ht[1]大小为 第一个大于等于ht[0]中已经保存的节点数的2的n次幂整数。
2、将字典中的属性rehashix的值设置为0,表示正在执行rehash操作。
3、将ht[0]中键值对依次重新计算哈希值(索引值和散列值),并放到ht[1]数组对应的索引位置,每完成一个键值对的rehash之后rehashix的值需要加1。
4、当ht[0]中所有的键值对都迁移到ht[1]之后,释放ht[0],并将ht[1]修改为ht[0],然后再创建一个新的ht[1]空数组,为下一次rehash做准备。
5、将字典中的属性rehashix设置为-1,表示rehash已经结束。

在上面操作的第三步时,为了避免当ht[0]中的键值对较多时一次性迁移带来的强大耗时,Redis 采用渐进式的方式完成 rehash 操作
当负载因子到达阀值后,只申请内存空间,暂时不移动键值对,而是在之后redis每次执行命令时,就从ht[0]中取出一个放到ht[1]中去,并将rehashix增一。经过多次操作,一点一点的完成迁移工作。

Hash 的 ziplist 实现同 上述的 ziplist 压缩列表结构,不同于List中ziplist的一点就是 Hash 的 ziplist entry 不再是具体的字符串,而是一个key-value键值对,key和value紧挨着一起。

Hash 常用操作命令

HDEL key field2 [field2]  #用于删除一个或多个哈希表字段
HEXISTS key field  #用于确定哈希表字段是否存在
HGET key field	#获取key关联的哈希字段的值
HGETALL key	 #获取key关联的所有哈希字段值
HINCRBY key field increment	 #将与key关联的哈希字段做整数增量运算
HINCRBYFLOAT key field increment  #将与key关联的哈希字段做浮点数增量运算
HKEYS key  #获取key关联的所有字段和值
HLEN key  #获取key中的哈希表的字段数量。
HMSET key field1 value1 [field2 value2 ]  #在哈希表中同时设置多个field-value(字段-值)
HMGET key field1 [field2]  #用于同时获取多个给定哈希字段(field)对应的值
HSET key field value  #用于设置指定 key 的哈希表字段和值(field/value)
HSETNX key field value  #仅当字段 field 不存在时,设置哈希表字段的值
HVALS key  #用于获取哈希表中的所有值
HSCAN key cursor  #迭代哈希表中的所有键值对,cursor 表示游标,默认为 0

场景应用

1,商品信息展示,定义商品信息的key为items:10001;key对应的filed 记录商品的ID,名称,描述,价格等信息(ID: 11 name: xxx content: xxxxxx price: 200)
2,存储经常变动的对象信息,如购物车

相关问题解答

1,在rehash过程中,进行了增删改查怎么办?
1》,执行增加操作时,新的节点会直接添加到ht[1]表中,这样保证ht[0] 的节点数量在整个rehash 过程中都只减不增,保证ht[0]随着rehash逐渐变为空表。
2》,因为在rehash 时,字典会同时使用两个哈希表,所以在这期间的所有查找、删除、更新等操作,除了在ht[0] 上进行外,还需要在ht[1] 上进行一遍。

Set 集合

Set 是一个无序并唯一的键值对集合。它的存储顺序不按照插入的先后顺序存储。一个集合最多可存储232-1个元素。Redis里的 set 除了支持集合内数据的增删改查操作外,还实现了基础的集合并、交、差的操作。
Set 的内部实现有两种编码方式:整数集合(intset) 和 哈希表(hashtable) 。

实现条件
1,集合的编码实现取决于第一个添加进集合的元素,如果第一个添加进集合的元素类型是long 类型的,那么编码就使用第一种,否则使用第二种
2,当insset保存的数值个数超过server.max_intset_entries(默认是512)时,编码方式将转化为hashtable
3,intset中全部是整数值,如果后续插入的数据类型不是long类型,intset也会转化成hashtable存储

intset 整数集合

intset 用于存储有序的,无重复的多个整数值,它会根据元素的值选择该用什么长度的整数类型保存元素,比如当整数集合中的元素都是int16_t 保存,当插入一个int32_t的元素时,会先将之前的所有元素升级为int32_t,再插入这个元素,所以整数集合会自动升级。

typedef struct intset {
    uint32_t encoding; //数据编码,表示intset中的每个数据元素占用几个字节来存储,有三种取值:INTSET_ENC_INT16表示每个元素用2个字节存储,INTSET_ENC_INT32表示每个元素用4个字节存储,INTSET_ENC_INT64表示每个元素用8个字节存储
    uint32_t length; //intset中的元素个数
    int8_t contents[]; //保存数据元素的数组。这个数组的总长度(总字节数)= encoding * length
} intset;

intset 编码升级步骤
1,根据新添加元素的类型来扩展底层数组空间的大小,为新元素分配内存空间。
2,将数组原有的元素都修改成新元素的类型,将转换类型后的元素从后到前逐个重新放回到数组内,保证原来数组的有序性。
3,将新元素放到数组合适的位置(小的放头部或大的放尾部)。
4,修改encoding为新的编码和length的长度值,完成升级。

注意

1,新创建的intset使用占内存最小的INTSET_ENC_INT16(值为2)作为数据编码。
2,每添加一个新元素,则根据元素大小决定是否对数据编码进行升级。编码升级后不可逆。

Set 中的 hashtable 实现同上述的 hashtable 哈希表。

Set 常用操作命令

SADD key member1 [member2]  #向集合添加一个或多个成员
SCARD key  #获取集合的成员数
SDIFF key1 [key2]  #返回第一个集合与其他集合之间的差异。
SDIFFSTORE destination key1 [key2]  #返回给定所有集合的差集并存储在destination中
SINTER key1 [key2]  #返回给定所有集合的交集
SINTERSTORE destination key1 [key2]  #返回给定所有集合的交集并存储在destination中
SISMEMBER key member  #判断member元素是否是集合key的成员
SMEMBERS key  #返回集合中的所有成员
SMOVE source destination member  #将member元素从source集合移动到destination集合
SPOP key  #移除并返回集合中的一个随机元素
SRANDMEMBER key [count]  #返回集合中一个或多个随机数
SREM key member1 [member2]  #移除集合中一个或多个成员
SUNION key1 [key2]  #返回所有给定集合的并集
SUNIONSTORE destination key1 [key2]  #所有给定集合的并集存储在destination集合中
SSCAN key cursor [MATCH pattern] [COUNT count]  #迭代集合中的元素

场景应用

1,标签系统,为用户添加标签,计算不同用户的共同感兴趣的标签。
2,用户关注系统,计算不同用户的共同关注对象,用户的粉丝列表,用户间的互相关注。

Sort Set 有序集合

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数 (score) 。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数 (score) 可以重复。

有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。
当数据比较少时,有序集合使用的是 ziplist 存储的,在以下情况下则会转化成 skiplist 存储。

1,ziplist保存的元素个数超过server.zset_max_ziplist_entries(默认是128)时
2,ziplist的元素长度大于server.zset_max_ziplist_value(默认是64字节)时

skiplist 跳跃表

跳跃表也简称跳表,它是一种有序的数据结构,在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
skiplist 主要由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,zskiplist结构则用于保存跳跃表节点的相关信息,如跳跃表节点数,表头节点指针,表尾节点指针等。

typedef struct zset {
    dict *dict; //字典对象
    zskiplist *zsl; //跳跃表对象
} zset;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; //跳跃表的头节点和尾结点指针
    unsigned long length; //跳跃表的节点数
    int level; //所有节点中最大的层数
} zskiplist;

typedef struct zskiplistNode {
    sds ele; //元素,是一个sds字符串,且唯一不重复
    double score; //分值,是一个double类型的浮点数,从小到大的顺序排列,不同节点的分值可以重复
    struct zskiplistNode *backward; //后退指针,只有一个,每次只能后退到前一个节点
    struct zskiplistLevel { //跳跃表中的层,实质是一个数组,存放着多个指向其他节点的指针
        struct zskiplistNode *forward; //前进指针
        unsigned long span; //当前节点到下一个节点的跨度(跨越的节点数),指向null表示跨度为0
    } level[];
} zskiplistNode;

每一个节点的层数(level)是随机生成的一个介于1~32之间的数字,而且新插入一个节点不会影响其它节点的层数。
Redis 内部实现的 skiplist 与传统的 skiplist 的差异:
1,分数 score 可以重复,而传统的 skiplist 是不允许重复的。
2,排序比较时,不仅比较 score ,还比较数据本身的内容。当 score 相同时,则根据数据内容的ASCII码值进行排序。
3,第一层琏表是一个双向琏表,即每个节点都在这一层拥有后退指针,用于从表尾方向向表头方向迭代,为了方便以倒序的方式获取一个范围内的元素。其他层都是单向琏表。

注意
Redis 在使用 skiplist 编码存储数据时,实际上是同时使用了skiplist跳跃表和 dict 字典分别来实现数据的存储,是为了提高排序和查找性能做的优化。skiplist用来根据分数查询数据及范围查找;dict用来查询数据到分数的对应关系,如果仅查找元素的话可以直接使用hash,其复杂度为O(1) 。
跳跃表结构可以使有序集支持以下操作:
1,根据score对member进行定位。
2,范围性查找和处理操作,高效地实现ZRANGE,ZRANK,ZINTERSTORE等命令。
字典结构以member作为键,score做为值,以下操作复杂度为O(1):1,检查给定的member是否存在于该有序集合中。
2,取出member对应的score值(ZSCORE命令)。

Zset 的 ziplist 实现如上述List对象的 ziplist 压缩列表结构 ,因为 ziplist对 member 和 score 是按照 score 从小到大的次序依次存储的,此处 entry 结构体中存储的数据样式如 member1 score1 member2 score2所示,第一个节点保存 member,第二个节点保存 score,依次类推。所以其检索的时候复杂度为O(n)。

Zset 常用操作命令

ZADD key score1 member1 [score2 member2]  #向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key  #获取有序集合的成员数
ZCOUNT key min max  #计算在有序集合中指定区间分数的成员数
ZINCRBY key increment member  #有序集合中对指定成员的分数加上增量increment
ZINTERSTORE destination numkeys key [key ...]  #计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合destination中
ZLEXCOUNT key min max  #在有序集合中计算指定字典区间内成员数量
ZRANGE key start stop [WITHSCORES]  #通过索引区间返回有序集合指定区间内的成员
ZRANGEBYLEX key min max [LIMIT offset count]  #通过字典区间返回有序集合的成员
ZSCORE key member  #返回有序集中,成员的分数值
ZRANK key member  #返回有序集合中指定成员的索引
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]  #通过分数返回有序集合指定区间内的成员
ZREM key member [member ...]  #移除有序集合中的一个或多个成员
ZREMRANGEBYLEX key min max  #移除有序集合中给定的字典区间的所有成员
ZREMRANGEBYRANK key start stop  #移除有序集合中给定的排名区间的所有成员
ZREMRANGEBYSCORE key min max  #移除有序集合中给定的分数区间的所有成员
ZREVRANGE key start stop [WITHSCORES]  #返回有序集中指定区间内的成员,通过索引,分数从高到低
ZREVRANGEBYSCORE key max min [WITHSCORES]  #返回有序集中指定分数区间内的成员,分数从高到低排序
ZREVRANK key member  #返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
ZUNIONSTORE destination numkeys key [key ...]  #计算给定的一个或多个有序集的并集,并存储在新的key中
ZSCAN key cursor [MATCH pattern] [COUNT count]  #迭代有序集合中的元素(包括元素成员和元素分值)

场景应用

1,各种排行榜系统,比如视频点赞数,播放排名,商品的销量排行等,基于 score 达到排序效果,分数相同时则按key的 ascii 码值排序。
2,商品的评价标签排序,记录商品的标签,统计标签次数,增加标签次数,按标签的分值进行排序

参考资料:
https://juejin.cn/post/6930520039560839176#heading-2
http://zhangtielei.com/posts/blog-redis-quicklist.html
https://blog.csdn.net/zwx900102/article/details/109707329
https://www.cnblogs.com/hunternet/p/9989771.html
Redis数据结构——整数集合
Redis数据结构——跳跃表

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值