redis核心与实战(一)数据结构篇

1.《redis数据结构概览》
1.数据结构概览
数据模型:一共5种,String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)

数据结构:一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。

对应关系图:

2.全局哈希表
Redis 使用了一个全局哈希表来保存所有键值对

哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

3.列表(list)
对应两种实现方法,一种是压缩列表(ziplist),另一种是双向循环链表。

当列表中存储的数据量比较小的时候,列表就可以采用压缩列表的方式实现。具体需要同时满足下面两个条件:

列表中保存的单个数据(有可能是字符串类型的)小于 64 字节;(cpu缓存行一行最多64字节)
列表中数据个数少于 512 个。
压缩列表:

特点:

可以存储不同字节大小的数据
基于数组实现,但不支持随机访问
获取首尾元素时间复杂度O(1),其他情况O(n)
压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度(所占空间大小)、列表尾的偏移量(列尾到起始位置的偏移量)和列表中的 entry 个数;

其中每个entry,分为四个部分previous_entry_length,entry_length,encoding,content

分别表示 上一个元素的长度,本元素长度 ,编码方式,文本内容

4.字典(hash)
有两种实现方式,一种是压缩列表,另一种是散列表

有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:

字典中保存的键和值的大小都要小于 64 字节;
字典中键值对的个数要小于 512 个。
5.集合(set)
有两种实现方法,一种是基于有序数组,另一种是基于散列表。

当要存储的数据,同时满足下面这样两个条件的时候,Redis 就采用有序数组,来实现集合这种数据类型。

存储的数据都是整数;
存储的数据元素个数不超过 512 个。
6.有序集合(sortedset)
当数据量比较小的时候,Redis 会用压缩列表来实现有序集合。具体点说就是,使用压缩列表来实现有序集合的前提,有这样两个:

所有数据的大小都要小于 64 字节;
元素个数要小于 128 个。
7.为什么 sortedset,hash,list都会用压缩列表呢 ?
当数据量小的时候通过数组下标访问最快、占用内存最小,而压缩列表只是数组的升级版;
因为数组需要占用连续的内存空间,所以当数据量大的时候,就需要使用链表了,同时为了保证速度又需要和数组结合,也就有了散列表。

1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

2.《redis 动态字符串详解》
1.案例:实现一个图片存储系统,并快速定位文件位置
案例描述:开发一个图片存储系统,要求这个系统能快速地记录图片 ID 和图片在存储系统中保存时的 ID(可以直接叫作图片存储对象 ID)。同时,还要能够根据图片 ID 快速查找到图片存储对象 ID。因为图片数量巨大,所以我们就用 10 位数来表示图片 ID 和图片存储对象 ID,

例如,图片 ID 为 1101000051,它在存储系统中对应的 ID 号是 3301000051。

photo_id: 1101000051
photo_obj_id: 3301000051
1
2
图片 ID 和图片存储对象 ID 正好一一对应,是典型的“键 - 单值”模式,这和 String 类型提供的“一个键对应一个值的数据”的保存形式刚好契合。

当我们储存一亿张图片时,用6.4G内存,但是,随着图片数据量的不断增加,我们的 Redis 内存使用量也在增加,结果就遇到了大内存 Redis 实例因为生成 RDB 而响应变慢的问题。

我们保存了 1 亿张图片的信息,用了约 6.4GB 的内存,一个图片 ID 和图片存储对象 ID 的记录平均用了 64 字节。但问题是,一组图片 ID 及其存储对象 ID 的记录,实际只需要 16 字节就可以了。

question:那么redis 的动态字符串类型为什么会使用这么多内存呢?另外48Byte 的内存用到哪里了呢?

2.动态字符串结构体 (Simple Dynamic String,SDS)
当我们用String类型键值对存储数据时,

当你保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,这种保存方式通常也叫作 int 编码方式。

当你保存的数据中包含字符时,String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存,如下图所示:

len: 4字节 ,表示buf 使用长度

alloc:4字节,表示 内存分配器分配空间 大小

buf:字节数组,保存实际数据,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。

3.RedisObject
对于 String 类型来说,除了 SDS 的额外开销,还有一个来自于 RedisObject 结构体的开销。

因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。

Long类型时: ptr引用指针直接填充赋值为整数数据这样就不用额外的指针再指向整数了,节省了指针的空间开销。这种布局方式也被称为 。
小于等于44字节的字符串:RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为
大于44字节的字符串时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为
下面是 三种编码方式的结构图:

所以,例子中,保存“1101000051:3301000051”只使用 了 32个字节,另外32字节消耗在哪里了?

这就要讲一下 redis 中的 dicEntry结构了

4.全局哈希表的 dicEntry结构
Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。

dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示:

但是,加上 这三个指针也才 24个字节 + 32字节 ,不够64字节啊,为什么?

应为redis 内存分配器 jemalloc,分配内存 按页分配,且分配 2的幂次方大小的内存页

所以,申请 了 56个字节,但是 内存分配器分配了 64字节

至此,保存“1101000051:3301000051”为什么分配了64字节就 真相大白了

那么,用什么来存储海量 的String类型,单向键值对呢?

3.压缩列表
1. ziplist结构
压缩列表(ziplist):表头有 zlbyts(4字节,列表所占字节大小),zltail(4字节,列尾到 列表其实位置的偏移量offset),zllen(2字节,列表entry元素个数); 最后在列表尾部有一个 zlend(1字节)表示列表结束

prev_len: 上一个元素 长度,prev_len 有两种取值情况:1 字节或 5 字节。

取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节;

否则,就取值为 5 字节。

len:当前元素长度,4字节

encoding: 编码方式,1字节

content:保存实际数据

如上图,展示了一个总长为80字节,包含3个节点的压缩列表。如果我们有一个指向压缩列表起始地址的指针p,那么表尾节点的地址就是P+60。

回到 存储海量图片并要求快速定位图片的例子:

每个 entry 保存一个图片存储对象 ID(8 字节),此时,每个 entry 的 prev_len 只需要 1 个字节就行,因为每个 entry 的前一个 entry 长度都只有 8 字节,小于 254 字节。这样一来,一个图片的存储对象 ID 所占用的内存大小是 14 字节(1+4+1+8=14),实际分配 16 字节。

2.使用ziplist好处以及 ziplist应用
Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的集合类型,这样做的最大好处就是节省了 dictEntry 的开销。当你用 String 类型时,一个键值对就有一个 dictEntry,要用 32 字节空间。但采用集合类型时,一个 key 就对应一个集合的数据,能保存的数据多了很多,但也只用了一个 dictEntry,这样就节省了内存。

3.如何用集合类型保存单值的键值对?
在保存单值的键值对时,可以采用基于 Hash 类型的二级编码方法。这里说的二级编码,就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了。

以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,我们可以把图片 ID 的前 7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value。

增加一条记录后,内存占用只增加了 16 字节:

127.0.0.1:6379> hset 1101000 060 3302000080
(integer) 1
1
2
4.什么时候使用ziplist?
Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表。

Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?其实,Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。

hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
为了能充分使用压缩列表的精简内存布局,我们一般要控制保存在 Hash 集合中的元素个数。所以,在刚才的二级编码中,我们只用图片 ID 最后 3 位作为 Hash 集合的 key,也就保证了 Hash 集合的元素个数不超过 1000,同时,我们把 hash-max-ziplist-entries 设置为 1000,这样一来,Hash 集合就可以一直使用压缩列表来节省内存空间了。

4.集合类型:一亿个key要统计怎么办?
总结


几种类型使用:

HyperLogLog:基数统计,集合元素量达到亿级别而且不需要精确统计

Bitmap:记录的数据只有 0 和 1 两个值的状态

Set 和 Sorted Set:都支持多种聚合统计(交并差集),不过,对于差集计算来说,只有 Set 支持

List和Sorted Set:

排序统计时,List 中的元素虽然有序,但是一旦有新元素插入,原来的元素在 List 中的位置就会移动,那么,按位置读取的排序结果可能就不准确了。而 Sorted Set 本身是按照集合元素的权重排序,可以准确地按序获取结果,所以建议你优先使用它。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凤舞飘伶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值