Redis的基础类型(三)

List列表

存储类型

存储有序的字符串(从左到右),元素可以重复。最大存储数量2^32-1(40亿左右)。

操作命令

lpush queue a
lpush queue b c
lpush queue d e
lpop queue
lpop queue
lindex queue 0
lrange queue 0 -1

存储原理

在早期的版本中,数据量较小时用ziplist存储(特殊编码的双向链表),达到临界值时转化为linkedList进行存储,分别对应OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_LINKEDLIST。

3.2版本之后,统一使用quickList来存储。quickList存储了一个双向链表,每个节点都是一个zipList,所以是zipList和LinkedList的结合体。

object encoding queue

quickList

总体结构:

quciklist.h 105行

typedef struct quicklist{

      quicklistNode *head; /* 指向双向列表的表头 */
      quciklistNode *tail;/* 指向双向列表的表尾 */
      unsigned long count;/* 所有的ziplist中一共存了多少个元素 */
      unsigned long len; /* 双向链表的长度,对应list-compress-depth */
      int fill:QL_FILL_BITS;/* ziplit最大大小,对应list-max-ziplist-depth */
      unsigned int compress:QL_COMP_BIT;/* 压缩深度,对应list-compress-depth */
      unsigned int bookmark_count:QL_BM_BITS;/* 4位,bookmarks数组的大小 */
      quciklistBookmark booksmarks[];/* bookmarks是一个可选字段。quciklist重新分配内存空间时使用,不使用时不占空间 */
} quicklist;

redis.conf相关参数:

参数 含义
List-max-ziplist-size(fill) 正数表示单个ziplist最多所包含的entry个数。负数表示单个ziplist的大小,默认8K。-1: 4KB; -2: 8KB; -3: 16KB; -4: 32KB; -5: 64KB
List-compress-depth(compress) 压缩深度,默认是0。 1:首尾的ziplist不压缩; 2:首尾第一个第二个ziplist不压缩

quicklist.h 46行


typedef struct quicklistNode{
      struct quicklistNode *prev; /* 指向前一个节点 */
      struct quicklistNode *next; /* 指向后一个节点 */
      unsigned char *zl; /* 指向实际的ziplist */
      unsigned int sz;  /* 当前ziplist占用了多少字节 */
      unsigned int count: 16; /* 当前ziplist中存储了多少个元素,占16bit,最大65536个 */
      unsigned int encoding: 2; /* 是否采用了LZF压缩算法压缩节点 */ /*RAW==1 or LZF==2 */
      unsigned int container: 2; /* 2:ziplist 是不是已经被解压出来作临时使用 */
      unsigned int attempted_compress: 1; /* 测试用 */
      unsigned int extra: 10; /* 预留给未来使用 */
} quicklistNode;

ziplist的结构:quicklist是一个数组 + 链表的结构。

应用场景

List主要用在存储有序内容的场景。

列表

用户的消息列表、网站的公告列表、活动列表、博客的文章列表、评论列表等。顺序储存显示。

队列/栈

List还可以当做分布式环境的队列/栈使用。
List还提供两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间(单位:秒)。

blpop queue
brpop queue

BLPOP:BLPOP key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOP:BRPOP key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

队列:先进先出 :rpush blpop , 左头右尾,右边进入队列,左边出队列。
栈:先进后出:rpush brpop
总结:

List存储有序的内容,用quicklist实现,本质上是数组 + 链表。
hashTable也是数组加链表,只是内部编码结构不一样。

Set集合

存储类型

Set存储String类型的无序集合,最大存储数量2^32-1(40亿左右)。

操作命令

//添加一个或者多个元素
sadd myset a b c d e f g
//获取所有元素
smembers myset 
//统计元素个数
scard myset
//随机获取一个元素
srandmember myset
//随机弹出一个元素
spop myset 
//移除一个或者多个元素
srem myset d e f
//查看元素是否存在
sismember myset a

存储(实现)原理
Redis用intset或者hashTable存储set。如果元素都是整数类型,就用intset存储。

insert.h 35行

typedef struct intset{
      unint32_t encoding; //编码类型 int16_t 、int32_t 、int64_t
      unint32_t length;  //长度 最大长度:2^32
      int8_t contents[]; //用来存储成员的动态数组
} intset;

如果不是整数类型,就用hashTable(数组 + 链表的存储结构)。
如果元素个数超过512个,也会用hashTable存储。跟一个配置有关:

set-max-insert-entries 512

QA: set的key没有value,怎么用hashTable存储? value这里存储的是null。

应用场景

抽奖

随机获取元素: spop myset

点赞、签到、打卡

商品标签

商品筛选

//获取差集
sdiff set1 set2
//获取交集(intersection)
sinter set1 set2
//获取并集
sunion set1 set2

用户关注、推荐模型

ZSet有序集合

存储类型

sorted set 存储有序的元素。每个元素有个score ,按照score从小到大排名。
score 相同时。按照key的ASCII码排序。

数据结构对比:

数据结构 是否允许重复元素 是否有序 有序实现方式
列表list 索引下标
集合set
有序集合zset 分值score

操作命令

//添加元素
zadd myset 10 Java 20 php 30 ruby 40 cpp
//获取全部元素
zrange myzset 0 -1 withscore
zrevrange myzset 0 -1 withscore
//根据分值区间获取元素
zrangebyscore myzset 20 30
//移除元素 也可以根据sore rank 删除
zrem myzset php cpp 
//统计元素个数
zcard myzset
//分值递增
zincrby myzset 5 python
//根据分值统计个数
zcount myzset 20 60
//获取元素rank
zrank myzset python
//获取元素score 
zscore myzset python

存储原理

默认使用ziplist编码(前面多种结构的类型,hash的小编吗,quicklist的node,都是ziplist)。
在ziplist的内部,按照score排序递增来存储。插入的时候要移动之后的数据。
如果元素数量大于等于128个,或者任一member长度大于等于64字节使用skiplist+dict存储。

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

那么什么是skiplist(跳表)?

先看一下有序链表:

在这样一个链表中,如果我们要查找某个数据,那么需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到一个比给定数据大的节点位置。时间复杂度为O(n)。同样,我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。二分查找法只适用于有序数组,不适用于链表。

假如我们每相邻两个节点增加一个执政,让指针指向下下个节点(或者理解为有元素进入了第二层)。

这样所有新增加的链表连成一个新的链表,但它包含的节点个数只有原来的一半。

那么那些元素会进入到第二层呢?在插入一个数据的时候,决定要放到哪一层,取决于一个算法,源码t_zset.c 122行

int zslRandomLevel(void){
      int level = 1;
      while((random() && 0xFFFF) < ZSKIPLIST_P * 0xFFFF)
            level += 1;
      return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

现在我们查询数据的时候,可以先沿着这个新的链表进行查找。当碰到比待查数大的节点时,在到下一层进行查找。

比如,我们想要找15,查找的路径是沿着标红的指针所指向的方向进行的:
1. 15首先和3比较,比它大继续向后比较。
2. 但是15和19比较时,要比19小,因此回到下面的链表(原链表),与7在第一层的下一个节点11比较。
3. 15要比11大,沿下面的指针继续向后和19比较。15比19小,说明待查数据23在原链表中不存在。

在这个查找过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了。需要比较的节点数大概只有原来的一半。这就是跳跃表。

这里为什么不用AVL树或者红黑树?因为skiplist更加简洁。
因为level是随机的,得到的skiplist可能是这样的,有些在第四层,有些在第三层,有的在第二层、第一层。

我们看一下Redis里面skiplist的实现:

源码: server.h 904行

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;

应用场景(顺序会动态变化的列表)

排行榜

  • 百度热搜

  • 微博热搜

id为6001的新闻点击数加1: zincrby hotNews:20251111 1 n6001
获取今天点击最多的15条: zrevrange hotNews:20251111 0 15 withscores

原文地址:https://www.cnblogs.com/snail-gao/p/14288113.html