Redis存储结构

前言

Redis 作为一款高性能数据库,表现在:它接收到一个键值对操作后, 能以微秒级别的速度找到数据,并快速完成操作。其高性能得奥秘来缘于以下两点:

  • Redis 是内存数据库, 所有操作都在内存上完成,内存的访问速度本身就很快
  • Reids 通过高效的数据结构来组织数据。

本章节可以让你在最短的时间了解如下内容:

  • Redis 支持五大数据类型
  • Redis 组织 Key-Value 数据结构
  • Redis5 大值类型数据存储结构

一、五大数据类型

  • String(字符串)
  • List(列表)
  • Set(集合)
  • Hash(哈希)
  • Zset(有序集合)

二、组织 Key-Value 数据结构

在 redis 中无论什么数据类型,在数据库中都是以 key-value 形式保存,通过进行对 Redis-key 的操作,来完成对数据库中数据的操作。

全局哈希表

为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。
一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一 个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。在下图中,可以看到,_哈希桶中的 entry 元素中保存了 key 和_value 指针,分别指向了 实际的键和值
因为这个哈希表保存了所有的键值对,所以,我也把它称为
全局哈希表**。
image.png

哈希表优势

让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算 键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。也就是说, 不管哈希表里有 10 万个键还是 100 万个键,我们只需要一次计算就能找到相应的键。

哈希表劣势

当往哈希表写入大量数据后,就可能发现操作有时候会突然变慢了,这其实是因为你忽略了一个潜在 的风险点,那就是哈希冲突问题和 rehash 可能带来的操作阻塞。

哈希冲突

当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。
哈希冲突,也就是 指,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。毕竟,哈希桶的个数通常要少于 key 的数量,这也就是说,难免会有一些 key 的哈希值对 应到了同一个哈希桶中。

链式哈希

链式哈希也很容易理解,就是指同一个哈希 桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
image.png
如上图所示:entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突。此 时,entry1 元素会通 过一个_next 指针指向 entry2,同样,entry2 也会通过_next 指针 指向 entry3。这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素 中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。

rehash

哈希冲突链上的元素只能通过指针逐一查找再操作。如果 哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链 过长,进而导致这个链上的元素查找耗时长,效率降低。
Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐 增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个 桶中的冲突。

简单 rehash

为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希 表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空 间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

  • 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  • 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中
  • 释放哈希表 1 的空间

到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来 的哈希表 1 留作下一次 rehash 扩容备用。(有点类似 JVM 年轻代复制回收)

渐进式 rehash

简单 rehash 过程中最大的问题在于第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都 迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据 了。为了避免这个问题,Redis 采用了渐进式 rehash。
渐进式 rehash 简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求 时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝 到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操 作,保证了数据的快速访问。
image.png

内存使用大小

哈希表的每一项是 一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针, 分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节。
但这里需要注意的是这里 Redis 使用的内 存分配库 jemalloc 了。
jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。
举个例子。如果你申请 6 字节空间,jemalloc 实际会分配 8 字节空间;如果你申请 24 字 节空间,jemalloc 则会分配 32 字节。所以,在我们刚刚说的场景里,dictEntry 结构就占 用了 32 字节
image.png

三、不同数据类型存储结构

Redis 底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈 希表、跳表和整数数组。它们和数据类型的对应关系如下图所示
image.png

简单动态字符串(SDS)

Redis String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存。

Redis SDS 数据结构

image.png

元数据

Redis 简单字符串可以存储数据类型有很多,而且,不同数据类型都有些相同的元数据要记录,这其中包括:

  • 最后一次访问的时间
  • 被引用的次数
  • ….

其中元数据占用 8 个字节。

PRT

表示一个指针,指针再进一步指向具体 数据类型的实际数据所在。如果存储类型是 String 类型,那么指针指向的是的 SDS 结构所在的内存地址。
其中 PRT 指针占用 8 个字节。

SDS

SDS 存储结构负责存储具体数据。其中包括了三层结构:

buf
  • 字节数组,保存实际数据。
  • 为了表示字节数组的结束,Redis 会自动在数组最后加 一个“\0”,这就会额外占用 1 个字节的开销。
len
  • 占 4 个字节,表示 buf 的已用长度。
alloc
  • 也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。

[图片上传失败…(image-ec3716-1611492954707)]

Redis SDS 内存优化

为了节省内存空间,Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计。

Long 类型整数

当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据 了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。

字符串类型
当字符串小于 44 字节时

RedisObject 中的元 数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被 称为 embstr 编码方式。

当字符串大于 44 字节时

SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。 这种布局方式被称为 raw 编码模式。
image.png

Redis SDS 优势

SDS 这种数据结构相对于 C 字符串有以下优点:

  • 杜绝缓冲区溢出
  • 减少字符串操作中的内存重分配次数
  • 二进制安全
  • 由于 SDS 遵循以空字符结尾的惯例,因此兼容部门 C 字符串函数

时间复杂度

操作 时间复杂度
获取 SDS 长度 由于 SDS 中提供了 len 属性,因此我们可以直接获取时间复杂度为 O(1),C 字符串为 O(n)。
获取 SDS 未使用空间长度 O(1)
清除 SDS 保存的内容 由于惰性分配策略,O(1)
创建一个长度为 N 字符串 O(n)
拼接一个长度为 N 的 C 字符串 O(n)
拼接一个长度为 N 的 SDS 字符串 O(n)

双向链表

数据结构-链表

相比数组,链表是一种稍微复杂一点的数据结构。数组需要一块连续的内存空间来存储,对内存的要求比较高。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。
image.png
链表结构五花八门,其中三种最常见的链表结构,它们分别是:单链表双向链表循环链表

单链表

链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针 next。
image.png
其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。

插入&删除

而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的。
image.png

查询

但是,有利就有弊。链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点
你可以把链表想象成一个队伍,队伍中的每个人都只知道自己后面的人是谁,所以当我们希望知道排在第 k 位的人是谁的时候,我们就需要从第一个人开始,一个一个地往下数。所以,链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度。

循环链表

循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从我画的循环链表图中,你应该可以看出来,它像一个环一样首尾相连,所以叫作“循环”链表
image.png

和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。

优势

双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。如删除某个结点 q 需要知道其前驱结点
删除某个结点前驱结点
我们已经找到了要删除的结点,但是删除某个结点 q 需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表,直到 p->next=q,说明 p 是 q 的前驱结点。但是对于双向链表来说,这种情况就比较有优势了。因为双向链表中的结点已经保存了前驱结点的指针, 不需要像单链表那样遍历。所以,针对第二种情况,单链表删除操作需要 O(n) 的时间复杂度,而双向链表只需要在 O(1) 的时间复杂度内就搞定了。
某个指定结点前面插入
同理,如果我们希望在链表的某个指定结点前面插入一个结点,双向链表比单链表有很大的优势。双向链表可以在 O(1) 时间复杂度搞定,而单向链表需要 O(n) 的时间复杂度。
查询某个节点
除了插入、删除操作有优势之外,对于一个有序链表,双向链表的按值查询的效率也要比单链表高一些。因为,我们可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。
Java 语言,你肯定用过 LinkedHashMap 这个容器。如果你深入研究 LinkedHashMap 的实现原理,就会发现其中就用到了双向链表这种数据结构。

设计思想

这里有一个更加重要的知识点需要你掌握,那就是用空间换时间的设计思想。当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。

双向循环链表

image.png

链表 VS 数组性能

数组和链表是两种截然不同的内存组织方式。正是因为内存存储的区别,它们插入、删除、随机访问操作的时间复杂度正好相反。

数组优势

数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。

数组劣势

数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。

取舍

如果你的代码对内存的使用非常苛刻,那数组就更适合你。因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片,如果是 Java 语言,就有可能会导致频繁的 GC(Garbage Collection,垃圾回收)。

Redis 链表结构

Redis 链表使用双向链表结构
image.png

  • 双向:链表节点带有前驱、后继指针获取某个节点的前驱、后继节点的时间复杂度为 0(1)。
  • 无环: 链表为非循环链表表头节点的前驱指针和表尾节点的后继指针都指向 NULL,对链表的访问以 NULL 为终点。
  • 带表头指针和表尾指针:通过 list 结构中的 head 和 tail 指针,获取表头和表尾节点的时间复杂度都为 O(1)。
  • 带链表长度计数器:通过 list 结构的 len 属性获取节点数量的时间复杂度为 O(1)。
  • 多态:链表节点使用 void*指针保存节点的值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用来保存各种不同类型的值。

压缩列表

压缩列表(zip1ist)是列表和哈希的底层实现之一。

  • 当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表的底层实现。
  • 当一个哈希只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做哈希的底层实现。

数据结构-压缩列表

听到“压缩”两个字,直观的反应就是节省内存。之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。我们知道,数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小(假设是 20 个字节)。存储小于 20 个字节长度的字符串的时候,便会浪费部分存储空间。
image.png
数组的优势占用一片连续的空间可以很好的利用 CPU 缓存访问数据。如果我们想要保留这种优势,又想节省存储空间我们可以对数组进行压缩。
image.png
是这样有一个问题,我们在遍历它的时候由于不知道每个元素的大小是多少,因此也就无法计算出下一个节点的具体位置。这个时候我们可以给每个节点增加一个 lenght 的属性。
image.png
如此。我们在遍历节点的之后就知道每个节点的长度(占用内存的大小),就可以很容易计算出下一个节点再内存中的位置。这种结构就像一个简单的压缩列表了。

Redis 压缩列表结构

image.png

zlbytes
  • 记录整个压缩列表占用的内存字节数;
  • 在对压缩列表进行重新内存分配,或者计算 zlleng 的位置时使用;
  • 占用 4 个字节;
zltail
  • 记录压缩列表表尾节点(entryN)起始偏移量距离压缩列表的起始地址有多少字节;
  • 通过这个偏移量,程序无须遍历整个压缩列表就可以确认压缩列表尾节点的地址;
  • 占用 4 个字节;
zllen
  • 记录压缩列表包含了节点(entryN)数量;
  • 当这个属性的小于 65535 时,这个属性的值就是压缩列表包含节点的数量;
  • 当这个属性的大于等于 65535 时,节点的真实数量需要遍历整个压缩列表才能计算获得;
  • 占用 2 个字节;
zlend
  • 特殊值 OxFF,用于记录压缩列表的未端;
  • 占用 1 个字节;
举个栗子

image.png

  • zllen 表示当前压缩列表有 3 个节点;
  • zltail 表示压缩列表表尾节点(entryN)起始偏移量距离压缩列表的起始地址有 60 个字节,因此如果表示 P 指向了压缩列表开始偏移量,那么 entry3 节点开始偏移量为 P+60;
  • zlbytes 表示整个压缩列表占用的内存字节数,因此通过计算可以知道 entry3 大小为 19=80-60-1;
entry 数据结构

列表节点

prev_len

表示前一个 entry 的长度。
表示上一个 entry 的长度小于 254 字节

  • 占用 1 字节
  • 前一节点的长度就保存在这一个字节里面

表示上一个 entry 的长度大于 254 字节

  • 占用 5 字节
  • 第一字节会被设置为 0xFE
  • 之后的四个字节则用于保存前一节点的长度
encoding
  • 表示数据的类型和长度
  • 占用 1 字节
len
  • 表示自身长度
  • 占用 4 字节
key
  • 保存实际数据

时间复杂度

操作 时间复杂度
创建一个压缩列表 O(1)
创建一个包含给定值的新节点,并将这个新节点添加到压缩列表的表头或者表尾 平均 O(N),最坏 O(N^2)(可能发生连锁更新)
将包含给定值的新节点插入到给定节点之后 平均 O(N),最坏 O(N^2)(可能发生连锁更新
返回压缩列表给定索引上的节点 O(N)
在压缩列表中查找并返回包含了给定值的节点 因为节点的值可能是一个字节数组,所以检查节点值和给定值是否相同的复杂度为 O(N),而查找整个泪飙的复杂度则(N^2)
返回给定节点的下一个节点 O(1)
返回给定节点的前一个节点 O(1)
获取给定节点所保存的值 O(1)
从压缩列表中删除给定的节点 平均 O(N),最坏 O(N^2)(可能发生连锁更新
删除压缩列表在给定索引上的连续多个 平均 O(N),最坏 O(N^2)(可能发生连锁更新
返回压缩列表目前占用的内存字节数 O(1)
返回压缩列表目前包含的节点数量 点数量小于 65535 时为 O(1),大于 65535 时为 O(N)

image.png

哈希表

跳表

整数数组

不同操作的复杂度

集合类型的操作类型很多,有读写单个集合元素的,例如 HGET、HSET,也有操作多个元 素的,例如 SADD,还有对整个集合进行遍历操作的,例如 SMEMBERS。这么多操作, 它们的复杂度也各不相同。而复杂度的高低又是我们选择集合类型的重要依据。
我总结了一个“四句口诀”,希望能帮助你快速记住集合常见操作的复杂度。这样你在使 用过程中,就可以提前规避高复杂度操作了

  • 单元素操作是基础;
  • 范围操作非常耗时;
  • 统计操作通常高效;
  • 例外情况只有几个。
单元素操作

第一,单元素操作,是指每一种集合类型对单个数据实现的增删改查操作。例如,Hash 类 型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等。这些操 作的复杂度由集合采用的数据结构决定,例如,HGET、HSET 和 HDEL 是对哈希表做操 作,所以它们的复杂度都是 O(1);Set 类型用哈希表作为底层数据结构时,它的 SADD、 SREM、SRANDMEMBER 复杂度也是 O(1)。
这里,有个地方你需要注意一下,集合类型支持同时对多个元素进行增删改查,例如 Hash 类型的 HMGET 和 HMSET,Set 类型的 SADD 也支持同时增加多个元素。此时,这些操 作的复杂度,就是由单个元素操作复杂度和元素个数决定的。例如,HMSET 增加 M 个元 素时,复杂度就从 O(1) 变成 O(M) 了。

范围操作

第二,范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据,比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS,或者返回一个范围内的部分数据,比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。这类操作的复杂度一般是 O(N),比较耗时, 我们应该尽量避免
不过,Redis 从 2.8 版本开始提供了 SCAN 系列操作(包括 HSCAN,SSCAN 和 ZSCAN),这类操作实现了渐进式遍历,每次只返回有限数量的数据。这样一来,相比于 HGETALL、SMEMBERS 这类操作来说,就避免了一次性返回所有元素而导致的 Redis 阻 塞。

统计操作

第三,统计操作,是指集合类型对集合中所有元素个数的记录,例如 LLEN 和 SCARD。这 类操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数 据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。

例外情况

第四,例外情况,是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头 和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操 作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂 度也只有 O(1),可以实现快速操作

四、经典案例

需求分析

当时,我们要开发一个图片存储系统,要求这个系统能快速地记录图片 ID 和图片在存储系 统中保存时的 ID(可以直接叫作图片存储对象 ID)。同时,还要能够根据图片 ID 快速查 找到图片存储对象 ID。
因为图片数量巨大,所以我们就用 10 位数来表示图片 ID 和图片存储对象 ID,例如,图片 ID 为 1101000051,它在存储系统中对应的 ID 号是 3301000051。

photo_id: 1101000051
photo_obj_id: 3301000051

可以看到,图片 ID 和图片存储对象 ID 正好一一对应,是典型的“键 - 单值”模式。所谓 的“单值”,就是指键值对中的值就是一个值,而不是一个集合,这和 String 类型提供 的“一个键对应一个值的数据”的保存形式刚好契合。

方案一

使用 String 类型

使用 String 保存数据。我们把图片 ID 和图片存储对象 ID 分别 作为键值对的 key 和 value 来保存,其中,图片存储对象 ID 用了 String 类型。

String 类型内存开销大

随着图片数据量的不断 增加,我们的 Redis 内存使用量也在增加,结果就遇到了大内存 Redis 实例因为生成 RDB 而响应变慢的问题。
当我们使用 String 类型时,除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等 信息,这些信息也叫作元数据。当实际保存的数据较小时,元数据的空间开销就显得比较 大了,有点“喧宾夺主”的意思。

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

计算 String 内存大小

因为 10 位数的图片 ID 和图片存储对象 ID 是 Long 类型整数,所以可以直接用 int 编码 的 RedisObject 保存
根据下图计算一共 64 字节。

结构 大小
key-value 哈希表 dictEntry 32 字节
图片 ID 简单字符串 SDS 元数据 8 字节
图片 ID 简单字符串 PRT 8 字节
图片存储对象 ID 简单字符串 SDS 元数据 8 字节
图片 ID 简单字符串 PRT 简单字符串 PRT 8 字节
https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. 前言
  2. 一、五大数据类型
  3. 二、组织 Key-Value 数据结构
    1. 全局哈希表
      1. 哈希表优势
      2. 哈希表劣势
    2. 哈希冲突
      1. 链式哈希
    3. rehash
      1. 简单 rehash
      2. 渐进式 rehash
    4. 内存使用大小
  4. 三、不同数据类型存储结构
    1. 简单动态字符串(SDS)
      1. Redis SDS 数据结构
        1. 元数据
        2. PRT
        3. SDS
          1. buf
          2. len
          3. alloc
      2. Redis SDS 内存优化
        1. Long 类型整数
        2. 字符串类型
          1. 当字符串小于 44 字节时
          2. 当字符串大于 44 字节时
      3. Redis SDS 优势
      4. 时间复杂度
    2. 双向链表
      1. 数据结构-链表
        1. 单链表
          1. 插入&删除
          2. 查询
        2. 循环链表
        3. 双向链表
          1. 优势
          2. 设计思想
        4. 双向循环链表
        5. 链表 VS 数组性能
          1. 数组优势
          2. 数组劣势
          3. 取舍
      2. Redis 链表结构
    3. 压缩列表
      1. 数据结构-压缩列表
      2. Redis 压缩列表结构
        1. zlbytes
        2. zltail
        3. zllen
        4. zlend
        5. 举个栗子
        6. entry 数据结构
          1. prev_len
          2. encoding
          3. len
          4. key
      3. 时间复杂度
    4. 哈希表
    5. 跳表
    6. 整数数组
    7. 不同操作的复杂度
      1. 单元素操作
      2. 范围操作
      3. 统计操作
      4. 例外情况
  • 四、经典案例
    1. 需求分析
    2. 方案一
      1. 使用 String 类型
      2. String 类型内存开销大
      3. 计算 String 内存大小