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