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

汇川PLC与EtherCAT伺服总线配置实战:从组态到故障排查 工业自动化中,运动控制系统的通信架构直接决定设备性能与调试效率。传统脉冲方式接线简单但受限于同步精度与轴数,Modbus虽通用却难以满足多轴联动,而EtherCAT凭借分布式时钟与微秒级同步机制,成为中高端装备的主流选择。针对汇川H5U、Easy系列PLC与SV660N/F伺服驱动的搭配,实际工程中常遇到扫描不到从站、使能失败、轴移动距离偏差等问题。本质上,这些故障多源于从站描述文件未正确导入、站号冲突、同步周期设置不合理、单位换算错误或物理层干扰。掌握InoProShop中的硬件组态、PDO映射、电子齿轮 阅读详情

继上篇讲解了字典的内部结构 之后,本篇我们开始讲字典 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。

640?wx_fmt=other
图片

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

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

typedef char* sds;

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;
    uint8_t alloc;
    unsigned char flags;
    char buf[]; // sds指向buf
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc;
    unsigned char flags;
    char buf[]; // sds指向buf
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc;
    unsigned char flags;
    char buf[]; // sds指向buf
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len;
    uint64_t alloc;
    unsigned char flags;
    char buf[]; // sds指向buf
};

#define SDS_TYPE_MASK 7
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

// SDS_HDR指向sdshdr<T>
// sds指向sdshdr<T>.buf

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1]; // 注意负下标
    switch(flags&SDS_TYPE_MASK) {
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

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

640?wx_fmt=png
图片

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

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

typedef struct redisObject {
    unsigned type:4; // 对象类型
    unsigned encoding:4; // 对象编码
    unsigned lru:24; // LRU时间戳
    int refcount; // 引用计数
    void *ptr; // 指向体结构的指针
} robj;

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

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

640?wx_fmt=other
图片
#define OBJ_ENCODING_RAW 0 // 普通 sds 字符串
#define OBJ_ENCODING_EMBSTR 8 // embstr

但是缓存行毕竟只有 64 字节,所以它能容纳的 sds 字符串的长度也是有限的。我们计算一下 redisObject 对象头需要占用 16 字节,最短的 sds 头部需要占用 3 字节,那么剩下的只有 45 字节了,字符串还需要以 NULL 结尾,最终留给字符串的长度最大也就只有 44 字节。我们可以通过 debug object 指令观察一下对象的编码类型来验证一下这个计算是否正确。

127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz012345678901234567
OK
127.0.0.1:6379> debug object codehole
Value at:0x7efed82220c0 refcount:1 encoding:embstr serializedlength:45 lru:2942469 lru_seconds_idle:4562936

127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz0123456789012345678
OK
127.0.0.1:6379> debug object codehole
Value at:0x7efed82704c0 refcount:1 encoding:raw serializedlength:46 lru:2942210 lru_seconds_idle:4563172

127.0.0.1:6379> set codehole 1024
OK
127.0.0.1:6379> debug object codehole
Value at:0x7efed824cb90 refcount:2147483647 encoding:int serializedlength:3 lru:2942982 lru_seconds_idle:4562541

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

#define OBJ_ENCODING_INT 1

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

typedef struct redisObject {
    unsigned type:4; // 对象类型
    unsigned encoding:4; // 对象编码
    unsigned lru:24; // LRU时间戳
    int refcount; // 引用计数
    void *ptr; // 指向体结构的指针
} robj;

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

typedef struct redisObject {
    unsigned type:4; // 对象类型
    unsigned encoding:4; // 对象编码
    unsigned lru:24; // LRU时间戳
    int refcount; // 引用计数
    long value; // 整数值
} robj;

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

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

127.0.0.1:6379> set codehole1 9999
OK
127.0.0.1:6379> set codehole2 9999
OK
127.0.0.1:6379> debug object codehole1
Value at:0x7efed826fc80 refcount:2147483647 encoding:int serializedlength:3 lru:2946566 lru_seconds_idle:4559814
127.0.0.1:6379> debug object codehole2
Value at:0x7efed826fc80 refcount:2147483647 encoding:int serializedlength:3 lru:2946566 lru_seconds_idle:4559819

127.0.0.1:6379> set codehole1 10000
OK
127.0.0.1:6379> set codehole2 10000
OK
127.0.0.1:6379> debug object codehole1
Value at:0x7efed821b080 refcount:1 encoding:int serializedlength:3 lru:2946823 lru_seconds_idle:4559610
127.0.0.1:6379> debug object codehole2
Value at:0x7efed821b160 refcount:1 encoding:int serializedlength:3 lru:2946822 lru_seconds_idle:4559613

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

for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
    shared.integers[j] = makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
    shared.integers[j]->encoding = OBJ_ENCODING_INT;
}

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

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

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    // redisObject对象头大小 + sds头部大小 + 字符串长度 + 1 (NULL结尾)
    // redisObject对象头指针
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    // o+1 实际上是 o + sizeof(redisObject)
    // sds头部指针
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    // sh+1 实际上是 sh + sizeof(struct sdshdr8)
    // 指向 sh->buf
    o->ptr = sh+1;
    o->refcount = 1;
    // 初始化 LRU bits
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    // 初始化字符串内容
    if (ptr == SDS_NOINIT)
        // 省去字符串拷贝时间
        sh->buf[len] = '\0';
    else if (ptr) {
        // 拷贝字符串
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        // 全部初始化为0,也就是空字符串
        memset(sh->buf,0,len+1);
    }
    return o;
}

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

本文节选之在线技术小册《Redis 深度历险》,对 Redis 感兴趣请点击「阅读原文」深入阅读《Redis 深度历险》

640?wx_fmt=jpeg

阅读更多深度技术文章,扫一扫上面的二维码关注微信公众号「码洞」

【3DSlicer】基于心脏ct影像重建3d模型及导出 软件:3DSlicer4.11 基于心脏ct影像重建3d模型 阅读详情

相关推荐

Ubuntu 24.04 新增用户并禁用 root 账户操作指南

Ubuntu 24.04 新增用户并禁用 root 账户操作指南

深山技术宅的博客 1860

redis OBJECT REFCOUNT 0-9999 会显示 (integer) 2147483647

阅读《redis设计与实现》时候,书中提到redis为了节省内存会在初始化服务器时,创建一万个字符串对象,这些对象包含了1到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是创建新的对象。 书中提到如果创建一个值为100的键A,并使用OBJECT REFCOUNT命令查看A的 引用计数: redis> SET A 100 OK redis> OBJECT REFCOUNT A (integer) 2 但是当在本机实操时候得到结果图如下:

菜菜的博客 2253

ParallelsDesktop 安装 CentOS-6.9-x86_64-minimal 教程

安装参考 tools没做 想安装的可以参考这个 mac 本机环境: 系统:macOS Catalina version 10.15.6 虚拟机软件:parallels Desktop 15 for Mac( Parallels Desktop-15.1.4(47270)) Centos镜像: CentOS-6.9-x86_64-minimal.iso https://mirrors.aliyun.com/centos-vault/6.8/isos/x86_64/ http://vault.centos.

爱吃鱼的小明的博客 955

redis设计与实现学习---(一)基础数据类型

一 基础数据类型 1 简单动态字符串(simple dynamic string,SDS) 与C中字符串的异同点: ① 获取字符串长度,C需要遍历字符数组,而SDS则通过len记录 ② 杜绝缓冲区溢出,C中如果合并两个字符串字符数组之前没有分配足够的空间就会造成数据截断,丢失数据,而SDS则合并之前判断free是否足够,不足够则会扩容在合并 ③ 因为有预留的free空间,所以在修改字符串的时候会减少内存重分配的次数(默认SDS长度小于1M则预分配free=len,如果大于1M则free=1M)。 ④ 惰性的

qq_39644692的博客 226

Keil5与ARM编译器V5安装指南:从下载到配置全流程解析

本文提供Keil5与ARM Compiler V5的完整安装与配置指南。针对新版Keil5默认不包含V5编译器导致老工程编译失败的问题,详细解析了从软件下载、授权管理到获取并正确安装ARM Compiler V5(AC5)的全流程,确保开发者能顺利搭建兼容老项目的稳定开发环境。

weixin_29158047的博客 877

Odoo 集成ReportBro报表设计器支持13,14,15,16,17,18,19

Odoo 集成ReportBro零代码报表设计器,ReportBro设计器 一个用于创建PDF和XLSX报告模板的javascript插件。

zxltwins的博客 3379

见微知著 —— Redis 字符串精致的内部结构

继上篇讲解了字典的内部结构之后,本篇我们开始讲字典 key 的内部结构,也就是 sds 字符串。首先它不是普通字符串,而是 sds 字符串,这个 sds 的意思是「Simple Dynamic String」,它的结构很简单,它是动态的,意味着可以支持修改。不过即使是这样简单的字符串结构,在结构设计上作者可是煞费苦心。 C语言里面的字符串是以 0x\0 结尾,通常我们就说以 NULL 结尾。它不包...

weixin_34348174的博客 205

研制过程评审活动(三)工程研制阶段

工程研制阶段主要任务、及评审活动

Abel的博客 2388

Redis 内存优化

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34301307的博客 179

学Simulink--基于Simulink的无迹卡尔曼滤波(UKF)目标跟踪仿真建模示例

本文详细介绍了基于Simulink的无迹卡尔曼滤波(UKF)目标跟踪仿真建模方法。首先分析了UKF相比EKF在强非线性场景下的优势,包括无线性化误差、计算效率高和鲁棒性强等特点。接着阐述了UKF的核心原理,重点讲解了无迹变换(UT)的三个关键步骤:Sigma点生成、非线性传播和加权统计重构。文章还详细说明了二维机动目标的状态向量定义、非线性状态转移模型和雷达观测模型。 在Simulink实现部分,提供了完整的建模步骤和核心代码,包括目标运动模型、雷达传感器模型和UKF算法模块的实现。通过蛇形机动场景的仿真实

xiaoheshang_123的博客 89

【沃顿商学院学习笔记】商业基础——Financing:05 名义利率和实际利率 APR& EAR

商业基础——名义利率和实际利率 本章主要是从名义利率和实际利率的相关概念和计算方法进行学习。 名义利率和实际利率 APR&amp;amp; EAR 根据下图可以直到: APR,QUOTED RATE在精算中又叫名义利率,比如一个房产贷款quoted rate or APR是12%,且按月复利,那么说明它的月实际利率是12%/12=1% EAR是effecive annual rate,在精算中叫实际利率,...

ECNU乐扣老师——区块链、可计算情感 1万+

redis的源码分析之不同编码类型的数据结构

1.1 简单动态字符串 Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示,它被用 在几乎所有的 Redis 模块中。 本章将对 sds 的实现、性能和功能等方面进行介绍,并说明 Redis 使用 sds 而不是传统 C 字符 串的原因。

陈嘉怡的专栏 1368

一张图看懂 ABAP CDS 两大子语言:CDS DDL 与 CDS DCL 以及缺席的 DML

ABAP CDS 的核心设计思想围绕语义数据模型(DDL)与数据访问控制(DCL)展开,而非单纯作为SQL查询工具。其DDL通过声明式语法定义业务导向的数据视图,强调模型稳定性与复用性;DCL则将访问控制逻辑下沉到模型层,实现行级权限过滤。ABAP CDS刻意剥离DML功能,交由ABAP SQL处理数据变更,形成清晰的分层架构。这种设计使数据定义、访问控制与应用逻辑解耦,为现代ABAP应用提供更安全、可维护的数据治理方案。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 976

码洞原创深度技术文章大全 —— 高端面试必备

《快学 Go 语言》第 11 课 —— 协程《快学 Go 语言》第 10 课 —— 错误与异常《快学 Go 语言》第 9 课 —— 接口《快学 Go 语言》第 8 课 —...

codehole_的博客 293

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

9月25日云栖精选夜读 | 王者荣耀使用什么编程语言开发的?

对于常见的3D游戏绝大部分用的unity3D游戏引擎开发而成,主要开发语言是C#.net技术,这是微软开发一套编程语言,当初推出来和java对抗的编程语言,现在C#使用范围其实非常广泛,java参与的地方几乎都有C#的影子,现在主要在桌面应用程序,游戏开发以及服务端等等都有涉猎,最近正好有个服务端的项目就准备用.net来实现,其实编程语言只是工具而已,最主...

weixin_33924220的博客 361

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

Redis深度历险-Redis字符串源码内部源码结构

本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!! Redis字符串结构 Redis中的字符串是可修改的字符串,在内存中是以字节数组形式存在的 SDS Redis中的字符串叫做“SDS”,也就是Simple Dynamic String,它的结构是带长度信息的字节数组;字符串是可以修改的字符串,支持append操作 struct SDS<T> { T capacity; //数组容量 T len; //数组长度 b

樊先知樊先知的博客 277

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

Redis 源码解析 string内部实现原理之简单动态字符串SDS

在redis里面,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方,比如打印日志。当redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值的时候,redis就会使用SDS来表示字符串值,比如 SET msg "hello world"RPUSH fruits "apple" "banana" "cherry" 1.SDS的定义 sturct sdshdr{

码厦(微信号) 630

Redis源码分析(二十八)--- object创建和释放redisObject对象

今天的学习效率比较高,把Rio分析完了,又顺便学习了其中的RedisObject的文件,只要讲的就是RedisObject的一些转换和创建。里面的大多数方法都是非常类似的。列出里面长长的API列表: /* ------------ API --------------------- */ robj *createObject(int type, void *ptr) /* 最初的创建robj对

走在前往架构师的路上 4683

Redis 源码解析 string内部实现原理之链表

在Redis中,链表提供了高效的节点重排能力,以及顺序性地节点访问方式,并且可以通过增删节点来灵活的调整链表的长度。比如redis中的列表结构就是使用了链表来实现,同时一些发布与订阅的需求也可以使用Redis的链表来实现。 下面给出Redis中链表和链表中节点的定义。 1.链表节点的结构定义 typedef struct listNode{ struct listNode *prev

码厦(微信号) 678

Redis基本数据类型以及String(一)

前言: Redis也有自己的数据类型,包含string,list,hash,set,sorted set。下面就对每种数据类型原理以及操作做一个详细的介绍。 Redis是面向编程的语言,除了字符串,其他类型怎么表示呢? Redis中定义了一个对象的结构体: /* * Redis 对象 */ typedef struct redisObject {...

a81561826的博客 467

Redis数据类型之redisObject

1、Redis中的object 为了便于操作,Redis定义了redisObjec结构体来表示string、hash、list、set、zset五种数据类型。redisObject定义在redis.h文件中: typedef struct redisObject { unsigned type:4; unsigned encoding:4; ...

小麦大大博客 7314

Redis(一)之 struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)))讲解

看Redis的源码的时候,我有点好奇,不知道 struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))) 是啥意思,所以就了解了下,首先我们看sdshdr的结构体 /* * 类型别名,用于指向 sdshdr 的 buf 属性 */ typedef char *sds; /* * 保存字符串对象的结构 */ struct sdshdr...

大洋深处 2517
上一篇: RPC 服务器之【多进程描述符传递】高阶模型
下一篇: 我们天天都在使用的管道命令,Shell 在里面到底动了什么手脚?
codehole_
博客等级 码龄7年 34粉丝 132原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值