Redis基础数据结构 | 小册免费学

281 阅读8分钟

Redis所有的数据结构都是key-value形式,并且key字符串是唯一的,不同类型的数据结构的差异就在于value的结构不一样,下面我们就来看看Redis有哪些基础数据类型吧

  • Redis基础数据结构
  • string(字符串)
  • list(列表)
  • set(集合)
  • zset(有序集合)
  • hash(哈希)

string

value为string的数据结构 Redis的字符串为动态字符串,是可以修改的字符串。内部结构是实现类似ArrayList,会动态扩容,并且会预分配冗余空间的方式来减少内存的频繁分配。一般会分配多余实际字符串长度的容量。

扩容机制:当字符串小于1M时,扩容都是加倍现有的空间,超过1M,扩容每次只会扩容1M,且字符串最大容量为512M

常用应用场景:缓存用户信息,通过序列化成JSON字符串存入Redis缓存,然后反序列化变成所需用户信息。

操作(在Redis客户端): 键值对 命令: 存值:set 键名 value值 取值:get 键名

批量键值对 可以批量读写多个字符串,节省网络耗时IO开销 命令: 存值:mset 键名1 v1 键名2 v2... 取值:mget 键名1 键名2

过期和set命令 过期命令:expire 键名 timeout 默认过期实践单位为秒 原子性存值+过期:set 键名 timeout value值

键存在返回1并执行set创建,键不存在返回0,set创建失败,setnx因为此特性,常被用来作为锁 setnx 键名 value值

计数 如果value是整数时,还可以对其自增,自增的范围为有符号的singed long的max和min,超过会报错 incr 键名 incrby 键名 步长(负数为自减)

list(列表)

Redis的列表结构为链表,插入,删除,时间复杂度为O(1),索引定位时间复杂度为O(n)。

而且列表弹出一个元素后,该数据结构被删除,内存被回收

使用场景: 经常使用左进右出数据结构做异步队列使用,将需要延后处理的任务塞进Redis列表,而另一个线程从这个列表中轮询数据进行处理

右进左出:队列 rpush 键名 v1 v2 ... 查看长度:llen books 取出数据:lpop books

右进右出:栈 rpush 键名 v1 v2 ... 取出数据:rpop books

慢操作 取出对应索引值:lindex 键名 去除区间外的值:ltrim 键名 start end 保留start-end区间的值,可以使用ltrim做一个定长的链表 index为负数时,-1为倒数第一,以此类推

快速链表 Redis列表的底层时一个quicklist结构 元素较少时:连续内存存储 结构:ziplist压缩列表 数据量较多时:将链表和ziplist结合形成quicklist,将多个ziplist使用双向指针连接 优点:每个元素都挂两个指针时会浪费空间,增重内存碎片化

hash(字典)

Redis的hash相当于Java的HashMap,无序字典

内部结构实现:数据加链表

第一维hash的数组碰撞后,将会将碰撞的元素用链表串起来

与Java中HashMap比较:Redis的字典值只能是string,rehash方式不同,Java的HashMap在字典很大时,rehash是个很耗时的操作,需要一次性全部rehash,Redis使用渐进式rehash策略,不堵塞服务。

渐进式rehash:在rehash的同时,保留了新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务以及hash操作指令中,循序渐进的将旧hash的内容迁移到新的hash结构中,当搬移完成了,新hash结构会取而代之

hash移除最后一个元素后,该数据结构自动被删除,内存被回收

hash结构也可以用来存储用户信息,string存储用户信息时需要全部序列化整个对象,hash可以对用户结构的每个字段单独存储,所以获取用户信息时就可以部分获取,可以节约网络流量。

hash缺点:hash结构存储消耗高于当个string

命令: 设置值:hset 键名 属性 属性值 取值:hgetall 键名 #entries()、hget 键名 属性名(取特定的属性值) 长度:hlen 键名

hash中单个属性值与string类似,可以进行自增 hincrby 键名 属性名 步长 incr 键名 属性名

set(集合)

Redis的集合可以类比于Java中的HashSet,他的内部键值对是无序的、唯一的

内部实现:特殊的字典,字典的每个value值为NULL

当集合中最后一个元素被移除后,数据结构自动删除,内存被回收

使用场景(去重):存储活动中奖用户,通过去重,保证用户不会中奖两次

命令 设置值: sadd 键名 value(可以多个value) 取值:smembers 键名 取一个:spop 键名 某个value是否存在:sismember 键名 value 长度:scard 键名

zset(有序集合)

Redis特有的数据结构,类似于Java中SortedSet和HashMap的结合体,set保证了value的唯一性,score代表value的权重,根据这个权重排序

内部实现:跳跃列表

zset最后一个value被移除后,数据结构被删除,内存被回收

应用场景(排序相关): 粉丝列表:value为ID,score为关注时间 学生成绩单:value为ID,score为成绩

命令 设置值:zadd 键名 权重 value 按权重排序列出:zrange 键名 start end (start end表示排序区间0 -1 表示所有) zrevrange 键名 start end 倒序 长度:zcard 键名 取某个值的权重score:zscore 键名 value 查看某个值的排名:zrank 键名 value 查看某个权重区间的值:zrangebyscore 键名 start end (start end 为权重区间) zrangebyscore 键名 -inf end withscores(-∞到end,并返回权重) 删除:zrem 键名 value

跳跃链表

zset内部实现:hash+跳跃链表

一个普通的链表查找元素的时间复杂度为O(N),即使单链表中的元素都是有序的,而数组可以利用二分查找的方法查找元素,面对有序的元素时,查找的时间复杂度为O(logn),而为了让链表查找效率更快,则出现了可以“二分查找”的链表:跳跃链表。

跳跃链表是一个有序的数据结构,每个节点维护了多个指向其他节点的指针,从而达到快速查找的目的。

当Redis中zset包含的元素比较多,或元素成员是比较长的字符串时,跳跃链表的优势会非常明显

跳跃链表可以理解为多层的链表

  • 多层的结构组成,每层都是一个有序的链表
  • 最底层(level 1)的链表包含了所有的元素
  • 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入,删除也为O(logn)
  • 跳跃表是一种随机化的数据结构(通过抛硬币的方式来决定层数)
  • 跳跃表的空间复杂度为O(logn)

如何让有序的链表查找速度更快呢?查找 因为元素都是有序的,我们可以再增加一些路径来加快查找速度。 路径:多加一层隔一个排序的有序链表 通过对上一层的元素与查找元素的比较(二分查找),决定是否进入下一层查找。

如何维护这个链表呢?插入与删除 插入或删除时,跳跃表的节点树就会改变,意味着跳跃表的层数也会动态改变

新插入的元素应该跨越几层呢? 采取策略:抛硬币,每次插入一层的一个节点时,抛一次硬币,不满足条件时统计次数,这个次数就是新插入元素的层数。

如何删除元素呢 直接把该元素跨域的层级删除即可

跳跃表vs二叉查找树 二叉查找树的查找、新增、删除也是近似O(logn),但存在一种极端情况,当数据有序时,此时就是一层有序的链表了,此时的查找、新增、删除的复杂度就变成O(n)了

跳跃表vs红黑树 红黑树在新增、删除节点时,会通过调整结构来保持红黑树的平衡,而跳跃表,通过一个随机数来决定跨域层级,由此可见红黑树的新增、删除时维护的花销是更高的

容器型数据结构的通用规则

list/set/zset/hash这四种容器型数据结构,共有的通用规则:

1.create if not exists 如果该容器未创建,那就新创建一个

2.dorp if no elements 如果该容器中已经没有元素了,就会删除这个容器的数据结构,释放内存

过期时间

Redis所有的基础数据结构都可以设置过期时间的,并且是以对象维度设置的。到期之后Redis会自动删除对应的对象,而不是对象中的某个元素值。

对于string的特殊性:当对一个string设置了过期时间,之后又修改了这个string,过期时间会消失