CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
复习Redis深度历险阅读笔记——万丈高楼平地起
0
Redis
小笨蛋
发布于:2022年04月15日
更新于:2022年04月17日
147
#custom-toc-container
### 基础:万丈高楼平地起 #### string (字符串) 字符串 string 是 Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。 ![图片alt](/uploads/images/20220415/220134-bc09f677e3794cc2b9446a4310066f16.png ''代码片段:Www.CodeSnippet.Cn'') 字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。我们将用户信息结构体使用 JSON 序列化成字符串,然后将序列化后的字符串塞进 Redis 来缓存。同样,取用户信息会经过一次反序列化的过程。 ![图片alt](/uploads/images/20220415/220207-67c5754b10d244abbb5bb979b1a8b8ad.png ''代码片段:Www.CodeSnippet.Cn'') Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 `ArrayList`,**采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 `capacity` 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。** ##### 计数 如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 `signed long` 的最大最小值,超过了这个值,Redis 会报错。 ------------ #### list (列表) Redis 的列表相当于 `Java` 语言里面的 `LinkedList`,注意**它是链表而不是数组**。这意味着 `list` 的插入和删除操作非常快,时间复杂度为 `O(1)`,但是索引定位很慢,时间复杂度为`O(n)`,这点让人非常意外。(链表的特点不就是插入删除快,查找慢吗?这个有什么意外的呢?) **当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。** **Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。** ##### 慢操作 `lindex` 相当于 `Java` 链表的 `get(int index)` 方法,时间复杂度`O(n)`,它需要对链表进行遍历,性能随着参数`index` 增大而变差。 `ltrim` 和字面上的含义不太一样,个人觉得它叫 `lretain`(保留) 更合适一些,因为 `ltrim` 跟的两个参数 `start_index` 和 `end_index` 定义了一个区间,**在这个区间内的值,`ltrim` 要保留,区间之外统统砍掉。**我们可以通过 `ltrim` 来实现一个定长的链表,这一点非常有用。`index` 可以为负数,`index=-1` 表示倒数第一个元素,同样 `index=-2` 表示倒数第二个元素。 ##### 快速列表 ![图片alt](/uploads/images/20220415/221651-d2e150d556a343289a5f3820221e4db3.png) 如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 `linkedlist`,而是称之为快速链表 `quicklist` 的一个结构。 首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 `ziplist`,也即是**压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 `quicklist`**。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 `int` 类型的数据,结构上还需要两个额外的指针 `prev` 和 `next` 。所以 Redis 将链表和 `ziplist` 结合起来组成了 `quicklist`。也就是将多个 `ziplist` 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。 ------------ #### hash (字典) Redis 的字典相当于 Java 语言里面的 `HashMap`,它是无序字典。内部实现结构上同 Java 的 `HashMap` 也是一致的,同样的**数组 + 链表二维结构**。**第一维 `hash` 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。** (解决哈希冲突的经典处理方式) ![代码片段](/uploads/images/20220415/225144-3d4ab77c91564fcbbed2e8d4aed2a606.png "代码片段") 不同的是,Redis 的字典的值只能是字符串,另外它们 `rehash`(哈希扩容) 的方式不一样,因为Java 的 `HashMap` 在字典很大时,`rehash` 是个耗时的操作,需要一次性全部 `rehash`。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 `rehash` 策略。 ![代码片段](/uploads/images/20220415/225344-bf5036f6cf4a46cfbfcad27eb4f2c181.png "代码片段") 渐进式 `rehash` 会在 `rehash` 的同时,保留新旧两个 `hash` 结构,查询时会同时查询两个`hash` 结构,然后在后续的定时任务中以及 `hash` 的子指令中,循序渐进地将旧 `hash` 的内容 一点点迁移到新的 `hash` 结构中。 **当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。** hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,**hash 可以对用户结构中的每个字段单独存储**。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。 hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。 同字符串一样,hash 结构中的单个子 key 也可以进行计数,它对应的指令是 `hincrby`,和 `incr` 使用基本一样。 ------------ #### set (集合) Redis 的集合相当于 Java 语言里面的 `HashSet`,它内部的键值对是无序的唯一的。**它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。** **当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。** set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。 ------------ #### zset (有序列表) zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的 `SortedSet` 和 `HashMap` 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 `score`,代表这个 value 的排序权重。**它的内部实现用的是一种叫着「跳跃列表」的数据结构。** zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。 zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。 zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。 ##### 跳跃列表 zset 内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比较复杂。 因为 zset 要支持随机的插入和删除,所以它不好使用数组来表示。我们先看一个普通的链表结构。 ![代码片段](/uploads/images/20220416/095141-dd6e6d3af604499099e8b8132e8b4426.png) 我们需要这个链表按照 score 值进行排序。这意味着当有新元素需要插入时,要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,只有数组才可以支持快速位置定位,链表做不到,那该怎么办? 想想一个创业公司,刚开始只有几个人,团队成员之间人人平等,都是联合创始人。随着公司的成长,人数渐渐变多,团队沟通成本随之增加。这时候就会引入组长制,对团队进行划分。每个团队会有一个组长。开会的时候分团队进行,多个组长之间还会有自己的会议安排。公司规模进一步扩展,需要再增加一个层级 —— 部门,每个部门会从组长列表中推选出一个代表来作为部长。部长们之间还会有自己的高层会议安排。 跳跃列表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。 想想你老家在世界地图中的位置:亚洲->中国->黑龙江->双鸭山市->尖山区->益人路->xxxx 号,也是这样一个类似的结构。 ![代码片段](/uploads/images/20220416/095506-72286ab73b494113bdcd08edfaaa64f6.png) 「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于 L0、L1 和 L2 层,可以快速在不同层次之间进行「跳跃」。 定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问,那新插入的元素如何才有机会「身兼数职」呢? 跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。 首先 L0 层肯定是 100% 了,L1 层只有 50% 的概率,L2 层只有 25% 的概率,L3 层只有 12.5% 的概率,一直随机到最顶层 L31 层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。 这还挺公平的,能不能进入中央不是靠拼爹,而是看运气。 ------------ #### 容器型数据结构的通用规则 `list/set/hash/zset` 这四种数据结构是容器型数据结构,它们共享下面两条通用规则: ##### 1、create if not exists 如果容器不存在,那就创建一个,再进行操作。比如 `rpush` 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 `rpush` 进去新元素。 ##### 2、drop if no elements 如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 `lpop` 操作到最后一个元素,列表就消失了。 #### 过期时间 Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。 还有一个需要特别注意的地方是**如果一个字符串已经设置了过期时间,然后你调用了set 方法修改了它,它的过期时间会消失。**
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024