Redis 的数据结构
底层数据结构
简单动态字符串SDS:
-
用于实现字符串,SDS 还被用作缓冲区:AOF 缓冲区,客户端状态中的输入缓冲区。
-
C语言字符串的区别和SDS(Simple Dynamic String)的数据结构(需要注意的是字符串最大长度为 512M)
-
常数复杂度获得字符串长度:C语言:获取长度需要遍历整个数组,O(n)复杂度;SDS只需要访问len属性就能得到字符串的长度,复杂度为O(1)。
-
杜绝缓冲区溢出:对于字符串的拼接、复制等操作,C语言开发者必须确保目标字符串的空间足够大,不然就会出现溢出;SDS由于有len,对字符串修改时候先根据len检查内存空间是否满足需 求,不满足则进行空间扩展不会发生溢出。
-
减少修改字符串长度所需的内存重分配次数:
-
空间预分配:可以减少连续执行字符串增长操作所需的内存分配次数。
- 空间预分配策略:sds存储的数据未超过1MB,长度扩容成原来的两倍;超过1MB,长度扩容1MB(避免空间浪费)
-
惰性空间释放:当需要缩短字符串时,不是立刻回收空间,而是用free字段记录未使用的空间(可用的长度大小)
-
-
二进制安全:C语言的String无法存储图片这样的(二进制数据),因为会对空格字符解释成字符串的结尾符;SDS额外实现了添加字符的API,不是沿用C语言的String API,不会对空格字符解释成字符串的结尾符。
-
链表:
-
用于实现列表键,还有发布、订阅、慢查询、监视器都用到了链表。
-
Redis的链表是双向链表
-
无环,表头节点的prev和表尾节点next指向null
-
带链表长度计数器
-
多态。使用 void* 指针来保存节点值,并通过 list 结构的 dup、free。match 三个属性为节点值设置 类型特定函数。
字典:
-
用于实现哈希键,还有数据库底层。
-
Redis 的字典使用哈希表作为底层实现,每个哈希表节点就保存了字典中的一个键值对。
-
使用链地址法解决哈希冲突
-
在堆哈希表扩展或者收缩时,程序需要将所有键值对rehash到新哈希表上,并且这个过程不是一次性的,而是渐进式完成的。
【渐进式 rehash】
-
维持一个索引计数器变量 rehashidx ,表示 rehash 工作正式开始, 并将它的值设置为 0 ,在每次增删改查时都会触发rehash 。
-
在 rehash 进行期间, 每次对字典执行增删改查, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有entry键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
-
在 rehash 过程中,新增操作,则直接写入 ht[1],查询、修改和删除则会在dict.ht[0] 和 dict.ht[1] 依次查找并执行(数据要么在ht0要么在ht1)。这样可以确保 ht[0] 的数据只减不增,随着 rehash 最终为空。
-
完成后程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
-
跳表:
- 用于实现有序集合。
- 跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。
- 跳跃表支持平均O(logN)、最坏O(N)的查找。
- 查找过程:最高层开始遍历找到第一个节点 (最后一个比「我」小的元素,也就是第一个比「我」大的元素的前一个),然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最后一个比我「小」的元素)。
- 每个跳跃节点的层高都是1~32的随机数。
- 跳跃表中的节点按照分值大小排序,当分值相同时,按照成员对象的大小排序。
- 跳表与平衡树、哈希表的比较:
- 跳表和各种平衡树(如 AVL、红黑树、B 树等)的元素是有序排列的,而哈希表不是有序的。因 此,在哈希表上只能做单个 key 的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小 在指定的两个值之间的所有节点。
- 跳表属于一种以空间换时间的数据结构,从内存占用上来说,一般情况下,平衡树相较于跳表空间 复杂度更低。,并且平衡树稳定,跳表不稳定(最坏时间复杂度O(n))。
- 1、跳表的实现更加简单,不用旋转节点,相对效率更高
2、跳表在范围查询的时候的效率是高于红黑树的,因为跳表是从上层往下层查找的,上层的区域范围更广,可以快速定位到查询的范围(我认为是最重要的)
3、平衡树的插入和删除操作可能引发子树的调整、逻辑复杂,而跳表只需要维护相邻节点即可
4、查找单个key,跳表和平衡树时间复杂度都是O(logN)
整数集合:
-
用于实现集合键
-
整数集合的底层实现为数组,这个数组以有序、不重复的方式保存集合元素,在有需要的时候,程序会根据新添加元素的类型,改变整个数组的类型。
-
升级操作是为了节约内存。整数集合只支持升级,不支持降级。
【升级添加新元素】
- 根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;
- 把数组现有的元素都转换成新元素的类型,并将转换后的元素放到正确的位置,且要保持数组的有序性;
- 添加新元素到底层数组;
压缩列表:
-
用于实现列表键和哈希键
-
压缩列表是redis为了节约内存开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构。
-
可以在任意一端进行压入、弹出操作,并且该操作的时间复杂度为O(1)。
-
压缩原理:1. 节约指针开销prev_len; 2. ,每个节点不同数据类型不同编码,长度不同。
-
连锁更新:添加节点或者删除节点,可能会引发连锁更新问题,但出现几率并不高,后续可以用quickList解决。
每个节点都有prev_len属性记录了前一个节点的长度。根据前一个节点的长度大小分配空间不一样。
添加元素:new_255 e1_253 e2_253 e3_253 …连锁更新
删除元素:big_255 samll_253 e2_253 e3_253 …连锁更新
【quicklist】
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
对象:
-
Redis数据库中每个键值对的键和值都是一个对象,共有五种对象。
-
Redis 可以在执行命令之前, 根据对象的类型来判断一个对象是否可以执 行给定的命令。 使用对象的另一个好处是, 我们可以针对不同的使用场景, 为对象设置多种不同的数 据结构实现, 从而优化对象在不同场景下的使用效率。
-
字符串对象:SDS
-
列表对象:ziplist或者linkedlist
-
哈希对象:ziplist或者hashtable
-
集合对象:intset或者hashtable
-
有序集合对象:ziplist或者skiplist
-
redis数据类型
-
String:String :String 是最常用的一种数据类型,采用 k-v 形式,一个 key 对应一个 value;string 类型是 二进制安全的。意思是 Redis 的 string 可以包含任何数据。如 jpg 图片或者序列化的对象 ;
使用场景:常规 key-value 缓存应用。常规计数:微博数, 粉丝数。
-
Hash:hash 是一个键值 (key => value) 对集合。Redis hash 是一个 string 类型的 field 和 value 的映 射表,hash 特别适合用于存储对象。
使用场景:常用于存储对象:用户信息,商品信息
hmset user:info age 30 name guanam page 50//批量获取hash key的一批field对应的值 O(n)
hmger user:info age name//O(n)
-"30"
-"guanam"
-
List:它的底层实际是个双向链表。
使用场景:
-
比如微博的关注列表, 粉丝列表, 消息列表
-
异步队列:使用list结构作为队列,rpush生产消息,lpop消费消息。可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。当lpop没有消息的时候,要适当sleep一会再重试。
可不可以不用sleep呢?list还有个指令叫blpop/brpop,在没有消息的时候,它会阻塞住直到消息到来。
-
-
Set:无序集合(内部使用值为空的哈希表),它通过计算hash的方式来快速去重,它能以 O(1) 的复杂度快速查询数据。
使用场景:
- 某些需要去重的列表,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
- 聚合计算(并集、交集、差集)场景,共同好友,共同关注。
-
ZSet:zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员;每个元 素都会关联一个 double 类型的分数;redis 正是通过分数来为集合中的成员进行从小到大的排序。 zset 的成员是唯一的,但分数(score)却可以重复。
使用场景:
- 排行榜相关:ZADD leaderboard 。 得到前100名高分用户很简单: ZREVRANGE leaderboard 0 99。
- 存储粉丝列表,value是用户id,score是关注时间。
- 延时队列:拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
ZSet由跳表+字典实现:
需要跳表:方便范围查找,zrange、zrank
需要字典:记录value和score关系,可以O(1)复杂度查找成员分值
-
Bitmap
-
底层:Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。
-
应用场景:需要保存状态信息(0/1 即可表示)的场景:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)
-
-
HyperLogLog
-
底层:用来做基数统计的算法,在输入元素的数量或体积非常大时,计算基数所需的空间总是固定的,并且是很小的(12k)。HyperLogLog 只会根据输入元素来计算基数,而不会存储输入元素本身
-
应用场景:数量量巨大(百万、千万级别以上)的计数场景:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、
-
-
Geospatial:主要用于存储地理位置信息,常用于定位附近的人,打车距离的计算等。