Redis 基础数据结构

397 阅读9分钟

Redis有5种基础的数据结构,分别是string(字符串),list(列表),hash(字典),set(集合)和zset(有序集合)。

string(字符串)

Redis中字符串是一种动态字符串,称为动态字符串(Simple Dynamic String 简称SDS).

底层结构

Redis的内存分配机制是这样:

  • 当字符串长度小于1MB时,每次扩容都是加倍现有空间。

  • 当字符串长度超过1MB时,每次扩容只会扩展1MB的空间(字符串的最大长度为512MB)

    String数据结构

上图就是字符串的基本结构,其中 content 里面保存的是字符串内容,0x\0作为结束字符不会被计算len中。

struct __attribute__ ((__packed__)) sdshdr{
  T alloc;       //数组容量
  T len;            //实际长度
  byte flags;  //标志位,低三位表示类型
  char[] content;   //数组内容
}

上面代码块是Redis的基础数据定义。其中alloclen都是泛型的定义。这样做的好处是因为当字符串比较短时,lenalloc可以使用byte和short表示,真的是对内存做到了极致的优化,不同长度的字符串使用不同的结构体表示。


常用的命令

命令 参数 描述
set [key] [value] 给指定key设置值(set 可覆盖老的值)
get [key] 获取指定key 的值
del [key] 删除指定key
exists [key] 判断是否存在指定key
mset [key1] [value1] [key2] [value2] ...... 批量存键值对
mget [key1] [key2] ...... 批量取key
expire [key] [time] 给指定key 设置过期时间 单位秒
setex [key] [time] [value] 等价于 set + expire 命令组合
setnx [key] [value] 如果key不存在则set 创建,否则返回0
getset [key] [value] 为 key 设置一个值并返回原值

演示一下这些命令:

redis-string命令执行结果图

list(列表)

底层结构

Redis 的列表相当于 Java 语言中的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。

源码定义:

// 节点数据结构
typedefstruct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
// 迭代器
typedefstruct listIter {
    listNode *next;
    int direction;
} listIter;

typedefstruct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsignedlong len;
} list;
list结构

但是,我们都知道链表的前后指针 prevnext 会占用较多的内存,会比较浪费空间。所以当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表),它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)结构。

ziplist

先看一下ziplist的数据结构

struct ziplist<T>{
    int32 zlbytes;            //压缩列表占用字节数
    int32 zltail_offset;    //最后一个元素距离起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength;            //元素个数
    T[] entries;            //元素内容
    int8 zlend;                //结束位 0xFF
}

压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一 个元素,然后倒着遍历。

应用场景

由于list它是一个按照插入顺序排序的列表,所以应用场景相对还较多的,例如:

  • 消息队列:lpoprpush(或者反过来,lpushrpop)能实现队列的功能
  • 朋友圈的点赞列表、评论列表、排行榜:lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表。

常用命令

命令 参数 备注
rpush [key] [value1] [value2] ...... 链表右侧插入
rpop [key] 移除右侧列表头元素,并返回该元素
lpop [key] 移除左侧列表头元素,并返回该元素
llen [key] 返回该列表的元素个数
lrem [key] [count] [value] 删除列表中与value相等的元素,
count是删除的个数。 count>0 表示从左侧开始查找,
删除count个元素,count<0 表示从右侧开始查找,
删除count个相同元素,count=0 表示删除全部相同的元素
lindex [key] [index] 获取list指定下标的元素 (需要遍历,时间复杂度为O(n))
lrange [key] [start_index] [end_index] 获取list 区间内的所有元素 (时间复杂度为 O(n))
end_index为-1表示倒数第一个元素

演示一下:

redis-list命令

hash(字典)

Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上。

HashString都可以用来存储用户信息 ,但不同的是Hash可以对用户信息的每个字段单独存储;String存的是用户全部信息经过序列化后的字符串,如果想要修改某个用户字段必须将用户信息字符串全部查询出来,解析成相应的用户信息对象,修改完后在序列化成字符串存入。而 hash可以只对某个字段修改,从而节约网络流量,不过hash内存占用要大于 String,这是 hash 的缺点。

应用场景

  • 购物车:hset [key] [field] [value] 命令, 可以实现以用户Id商品Idfield,商品数量为value,恰好构成了购物车的3个要素。

  • 存储对象:hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。

常用命令

命令 参数 备注
hset [key] [field] [value] 新建字段信息
hget [key] [field] 获取字段信息
hdel [key] [field] 删除字段
hlen [key] 保存的字段个数
hgetall [key] [慎用]获取指定key 字典里的所有字段和值 (字段信息过多,会导致慢查询 )
hmset [key] [field1] [value1] [field2] [value2]...... 批量创建
hincr [key] [field] 对字段值自增
hincrby [key] [field] [number] 对字段值增加number

演示一下:

redis-hash操作.png

set(集合)

Redis 中的 setJava中的HashSet 有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。

应用场景

好友、关注、粉丝、感兴趣的人集合:

  1. sinter命令可以获得A和B两个用户的共同好友;
  2. sismember命令可以判断A是否是B的好友;
  3. scard命令可以获取好友数量;
  4. 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合

首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。

存储某活动中中奖的用户ID ,因为有去重功能,可以保证同一个用户不会中奖两次。

常用命令

命令 参数 备注
sadd [key] [value] 向指定key的set中添加元素
smembers [key] 获取指定key 集合中的所有元
sismember [key] [value] 判断集合中是否存在某个value
scard [key] 获取集合的长度
spop [key] 弹出一个元素
srem [key] [value] 删除指定元素
srandmember [key] [n] 从中随机获取n个元素
sinter [key1] [key2] 获得key1和key2的交集

演示一下:

redis-set命令.png

zset(有序集合)

zset也叫SortedSet一方面它是个 set ,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score,代表这个value的排序权重。它的内部实现用的是一种叫作“跳跃列表”的数据结构。

跳跃链表数据结构

可能看图会有点懵,为什么要长成这样。现在打个比方:

想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人又是员工又有组长的身份

再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。

跳跃链表数据举例图

跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。最终形成了一个金字塔的结构。


应用场景

zset 可以用做排行榜,但是和list不同的是zset它能够实现动态的排序,例如: 可以用来存储粉丝列表,value 值是粉丝的用户 ID,score 是关注时间,我们可以对粉丝列表按关注时间进行排序。

zset 还可以用来做微博的热搜榜。其中分数 = 人气热度 * 常量 + 发布时间(随着时间的流逝不断减少的评分)。这样我们就能根据分数获取一个热搜榜。

常用命令

命令 参数 备注
zadd [key] [score] [value] 向指定key的集合中增加元素
zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
zrevrange [key] [start_index] [end_index] 获取范围内的元素列表 ,按score排序 逆序输出
zcard [key] 获取集合列表的元素个数
zrank [key] [value] 获取元素再集合中的排名
zrangebyscore [key] [score1] [score2] 输出score范围内的元素列表
zrem [key] [value] 删除元素
zscore [key] [value] 获取元素的score
zcount[] [key] [min] [max] 获取分值介于min和max之间的成员数量

演示一下:

redis-zset命令.png

本文章大量引用掘金用户程序员那点事的Redis数据结构及对应使用场景,看一次就整明白得了这篇文章

以及微信公众号作者我没有三颗心脏者的Redis—5种基本数据结构