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的基础数据定义。其中alloc和len都是泛型的定义。这样做的好处是因为当字符串比较短时,len和alloc可以使用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 设置一个值并返回原值 |
演示一下这些命令:
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;
但是,我们都知道链表的前后指针 prev 和 next 会占用较多的内存,会比较浪费空间。所以当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表),它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)结构。
ziplist
先看一下ziplist的数据结构
struct ziplist<T>{
int32 zlbytes; //压缩列表占用字节数
int32 zltail_offset; //最后一个元素距离起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; //元素个数
T[] entries; //元素内容
int8 zlend; //结束位 0xFF
}
压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一 个元素,然后倒着遍历。
应用场景
由于list它是一个按照插入顺序排序的列表,所以应用场景相对还较多的,例如:
- 消息队列:
lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能 - 朋友圈的点赞列表、评论列表、排行榜:
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表示倒数第一个元素 |
演示一下:
hash(字典)
Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上。
Hash 和String都可以用来存储用户信息 ,但不同的是Hash可以对用户信息的每个字段单独存储;String存的是用户全部信息经过序列化后的字符串,如果想要修改某个用户字段必须将用户信息字符串全部查询出来,解析成相应的用户信息对象,修改完后在序列化成字符串存入。而 hash可以只对某个字段修改,从而节约网络流量,不过hash内存占用要大于 String,这是 hash 的缺点。
应用场景
-
购物车:
hset [key] [field] [value]命令, 可以实现以用户Id,商品Id为field,商品数量为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 |
演示一下:
set(集合)
Redis 中的 set和Java中的HashSet 有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
应用场景
好友、关注、粉丝、感兴趣的人集合:
sinter命令可以获得A和B两个用户的共同好友;sismember命令可以判断A是否是B的好友;scard命令可以获取好友数量;- 关注时,
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的交集 |
演示一下:
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数据结构及对应使用场景,看一次就整明白得了这篇文章
以及微信公众号作者我没有三颗心脏者的Redis—5种基本数据结构