见微知著 —— Redis 字符串内部结构源码分析

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 本篇我们开始讲字典 key 的内部结构,也就是 sds 字符串。首先它不是普通字符串,而是 sds 字符串,这个 sds 的意思是「Simple Dynamic String」,它的结构很简单,它是动态的,意味着可以支持修改。

本篇我们开始讲字典 key 的内部结构,也就是 sds 字符串。首先它不是普通字符串,而是 sds 字符串,这个 sds 的意思是「Simple Dynamic String」,它的结构很简单,它是动态的,意味着可以支持修改。不过即使是这样简单的字符串结构,在结构设计上作者可是煞费苦心。

我们知道 C语言里面的字符串是以0x\0结尾,通常就说是以 NULL 结尾。它不包含长度信息,当我们需要获取字符串长度时,需要调用 strlen(s) 来获取长度,它的时间复杂度是 O(n),如果一个字符串太长,这个函数就太浪费 CPU了。

所以 Redis 不能这么干,它需要将长度信息使用单独的字段进行存储,这就需要一个额外的字段,这个字段也要占用存储空间。在日常使用中,小字符串才是大头,它的长度信息往往只需要 1byte 存储就可以了,可以表示最大长度为 255 的字符串。如果字符串再大一些,就需要 2byte,甚至是 3byte、4byte。Redis 会为不同长度的字符串选择不同长度的字段来表示长度信息。同时 Redis 为了可以直接使用标准C语言字符串库函数,sds 的字符串内容还是以 NULL 结尾,这会额外多占用一个字节的空间。

sds 是动态字符串,它需要支持追加操作,需要能扩充容量。如果字符串放置的比较紧凑,追加时,就需要重新分配新的更大的存储空间,然后进行内容的拷贝(不严格,想想为什么)。如果追加的太频繁,内存的分配和拷贝就会消耗大量 CPU。

图片

所以 Redis 为动态字符串设计了冗余空间,追加时只要内容不是太大,是可以不必重新分配内存的,如果字符串的长度是1024,Redis 会分配2048字节的存储空间,也就是 100% 的冗余空间。这个设计非常类似于 Java 语言的 ArrayList 。不过 Redis 考虑的更加周到,当字符串的长度超过 1M 时,它的冗余空间只有 1M,避免出现太大的浪费。Redis 还限制了字符串最大长度不得超过 512M。

下面是 sds 字符串的结构定义源码

 

我们日常使用的字符串都是只读的,一般只有拿字符串当位图使用时才会对字符串进行追加和修改操作。为了避免浪费,Redis 在第一次创建 sds 字符串时,不给它分配冗余空间。在第一次追加操作之后才会分配 100% 的冗余空间。

图片

值得注意的是,我们平时使用的字符串指针都是指向字符串内存空间的头部,但是在 Redis 里面我们使用的 sds 字符串指针指向的是字符串内存空间的脖子部位,因为 sds 字符串有自己的头部信息。

如果 sds 字符串只是作为字典的 key 而存在,那么字典里面元素的 key 会直接指向 sds。如果 字符串是作为 Redis的对象而存在,它还会包上一个通用的对象头,也就是 RedisObject。对象头的 ptr 字段会指向 sds。

 

讲到这里,需要提一下现代计算机的结构上在 CPU 和 内存之间存在一个缓存的结构,用来协调 CPU 的高效和访存的相对缓慢的矛盾。我们平时听到的 L1 Cache、L2 Cache就是这个缓存。当 CPU 要访问内存时先在缓存里找一找有没有,如果没有就去内存里拿了之后放到缓存里,这个缓存的最小单位一般是 64 字节,也就是一次性缓存连续的 64 字节内容,这个最小单位称为「缓存行」。这样下次获取内存地址附近的数据时可以直接从缓存中拿到。

对于 Redis 的字符串对象来说,我们需要先访问 redisObject 对象头,拿到 ptr 指针,然后再访问指向的 sds 字符串。如果对象头和 sds 字符串相距较远,就会存在缓存穿透现象,性能就会打折。所以 Redis 为了优化硬件的缓存命中,它为字符串设计了一种特殊的编码结构,这种结构就是 embstr 。它将 redisObject 对象头和 sds 字符串挤在一起连续存储,可以一次性放到缓存行里,这样就可以明显提升缓存命中率。

 

 

object 指令观察一下对象的编码类型来验证一下这个计算是否正确。

 

注意到上面的输出中出现了 encoding:int 类型的编码,这是怎么回事呢?原来 Redis 又对整型字符串做了优化,当字符串是可以用 long 类型表达的整数时,Redis 内部将会使用整型编码。注意整数在 Redis 内部的类型 type 是字符串。

 

我们再观察一遍 redisObject 对象头。

 

当字符串内容可以用 long 整数表达时,对象头的 ptr 指针将退化为一个 long 型的整数。也就是

 

如果这个整数太大,超出了 long 的表达范围,就会使用 sds 字符串表示,根据长短不同会分别选择 embstr 和 raw 编码类型。

我们再看一个很诡异的现象

 

注意 debug object 指令输出的 Value at: xxxxxxx 这个表示 redisObject 对象头的地址。为什么值为 9999 时,两个对象的地址是一样的。而变成了 10000 地址就不一样了呢?

 

这是因为「小整数对象缓存」。Redis 在初始化的时候会构造 [0, 10000) 这1w个小整数对象持久放在内存里,以后凡是在这个范围内的整型字符串都会直接使用共享的小整数对象。小整数对象的引用计数字段的值恒定为 INT_MAX。在很多面向对象的语言中,都有小整数对象缓存的概念。

接下来我们仔细分析一下创建 embstr 的函数 createEmbeddedStringObject 的代码

 

我们可以看到对象头和字符串内容是通过一次zmalloc调用分配的,也就是说对象头和字符串内容是连续的分配在一起。还将 sds 字符串的 flags 设置为 SDS_TYPE_8 说明它是一个短字符串,长度可以直接用一个字节就可以表示。同时在字符串内容 buf 的尾部有 '\0' 标识,这是 C 字符串的结束标志。

欢迎工作一到五年的Java工程师朋友们加入Java架构开发:744677563

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
存储 缓存 NoSQL
redis数据结构-字符串
redis数据结构-字符串
35 1
|
5月前
|
存储 NoSQL Redis
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
81 1
|
17天前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
30 4
|
2月前
|
存储 缓存 NoSQL
3)深度解密 Redis 的字符串
3)深度解密 Redis 的字符串
28 1
|
6月前
|
存储 监控 NoSQL
Redis处理大量数据主要依赖于其内存存储结构、高效的数据结构和算法,以及一系列的优化策略
【5月更文挑战第15天】Redis处理大量数据依赖内存存储、高效数据结构和优化策略。选择合适的数据结构、利用批量操作减少网络开销、控制批量大小、使用Redis Cluster进行分布式存储、优化内存使用及监控调优是关键。通过这些方法,Redis能有效处理大量数据并保持高性能。
95 1
|
1月前
|
存储 NoSQL Redis
redis保存数据的结构-redisobject结构体
`redisObject`结构体是Redis内部数据组织的核心,它通过集成类型标识、引用计数和编码方式等关键信息,实现了数据的高效管理和访问。这种设计允许Redis根据数据的实际需求动态调整存储结构,既保证了内存使用的高效性,也确保了数据操作的灵活性和速度。通过对 `redisObject`的深入了解,可以更好地掌握Redis如何在内存中高效存储和操作数据,进而优化数据库的性能和资源利用。
18 0
|
3月前
|
NoSQL 安全 Java
Redis6入门到实战------ 三、常用五大数据类型(字符串 String)
这篇文章深入探讨了Redis中的String数据类型,包括键操作的命令、String类型的命令使用,以及String在Redis中的内部数据结构实现。
Redis6入门到实战------ 三、常用五大数据类型(字符串 String)
|
3月前
|
C# 开发者 UED
WPF开发者必备秘籍:深度解析文件对话框使用技巧,打开与保存文件原来如此简单!
【8月更文挑战第31天】在WPF应用开发中,文件操作是常见需求。本文详细介绍了如何利用`Microsoft.Win32`命名空间下的`OpenFileDialog`和`SaveFileDialog`类来正确实现文件打开与保存功能。通过示例代码展示了如何设置文件过滤器、初始目录等属性,并使用对话框进行文件读写操作。正确使用文件对话框能显著提升用户体验,使应用更友好易用。
68 0
|
3月前
|
存储 NoSQL Redis
【Redis 探秘】SDS 简单动态字符串:揭秘 Redis 高效字符串处理的秘密武器!
【8月更文挑战第24天】Redis采用的简单动态字符串(SDS)是一种专为优化内存存储和字符串操作而设计的数据结构。相较于C语言的标准字符串,SDS改进了字符串长度计算、内存重分配及字符串比较等问题。其特性包括预分配冗余空间减少未来的内存重分配、显式存储长度以加快获取速度等。这些改进使Redis能更高效地管理字符串数据。例如,在Redis中,SDS被广泛应用于键值对的存储,显著提升了字符串操作的性能。了解SDS不仅对于深入理解Redis的工作原理至关重要,也是开发者技能树中的重要一环。
55 0
|
3月前
|
存储 JSON NoSQL
揭秘Redis字符串String的隐藏技能!从基础到进阶,让你的数据存储操作秒变高大上!
【8月更文挑战第24天】Redis中的字符串类型作为其基石,不仅能够存储从简单文本到复杂格式如JSON的各种数据,还能通过多样化的命令实现包括但不限于自增自减、设置过期时间等高级功能,极大提升了其实用性和灵活性。例如,使用`SET`命令可添加或更新键值对,`GET`获取值,`DEL`删除键;同时,`INCR`和`DECR`支持对整数值的原子性增减操作,非常适合实现计数器等功能;通过`EXPIRE`命令设置过期时间,则适用于需要限时存储的应用场景。尽管名为“字符串”,但实际上还可存储图片、音频文件的Base64编码等形式的数据,为开发者提供了强大而灵活的工具。
47 0