Redis——简单动态字符串(Simple Dynamic Strings,SDS)

2023-12-05

简单动态字符串(Simple Dynamic Strings,SDS)是Redis的基本数据结构之一,用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

1、数据结构

在了解SDS源码前,我们先思考一个问题:如何实现一个扩容方便且二进制安全的字符串呢?

注意: 什么是二进制安全?通俗地讲,C语言中,用“ \0 ”表示字符串的结束,如果字符串中本身就有“ \0 ”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

SDS既然是字符串,那么首先需要一个字符串指针;为了方便上层的接口调用,该结构还需要记录一些统计信息,如当前数据长度和剩余容量等,例如:

    struct sds {
        int len; // buf中已占用字节数
        int free; // buf中剩余可用字节数
        char buf[]; // 数据空间
    };

SDS结构示意如下图所示,在64位系统下,字段len和字段free各占4个字节,紧接着存放字符串:
在这里插入图片描述
Redis 3.2之前的SDS也是这样设计的。这样设计有以下几个优点:

  • 有单独的统计变量len和free(称为头部)。可以很方便地得到字符串长度。
  • 内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。
  • 由于有长度统计变量len的存在,读写字符串时不依赖“ \0 ”终止符,保证了二进制安全。

注意: 上例中的buf[]是一个柔性数组。柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。

之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置);可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。

到这里我们实现了一个最基本的动态字符串,但是该结构是否有改进的空间呢?我们从一个简单的问题开始思考:不同长度的字符串是否有必要占用相同大小的头部?一个int占4字节,在实际应用中,存放于Redis中的字符串往往没有这么长,每个字符串都用4字节存储未免太浪费空间了。我们考虑三种情况:短字符串,len和free的长度为1字节就够了;长字符串,用2字节或4字节;更长的字符串,用8字节。

这样确实更省内存,但依然存在以下问题:

  • 问题1:如何区分这3种情况?
  • 问题2:对于短字符串来说,头部还是太长了。以长度为1字节的字符串为例,len和free本身就占了2个字节,能不能进一步压缩呢?

对于问题1,我们考虑增加一个字段flags来标识类型,用最小的1字节来存储,且把flags加在柔性数组buf之前,这样虽然多了1字节,但通过偏移柔性数组的指针即能快速定位flags,区分类型,也可以接受;对于问题2,由于len已经是最小的1字节了,再压缩只能考虑用位来存储长度了。

结合两个问题,5种类型(长度1字节、2字节、4字节、8字节、小于1字节)的SDS至少要用3位来存储类型(2^3=8),1个字节8位,剩余的5位存储长度,可以满足长度小于32的短字符串。在Redis 5.0中,我们用如下结构来存储长度小于32的短字符串:

    struct __attribute__ ((__packed__))sdshdr5 {
        unsigned char flags; /* 低3位存储类型,高5位存储长度 */
        char buf[]; /*柔性数组,存放实际内容*/
    };

sdshdr5结构(下图)中,flags占1个字节,其低3位(bit)表示type,高5位(bit)表示长度,能表示的长度区间为0~31(25-1), flags后面就是字符串的内容。
在这里插入图片描述
而长度大于31的字符串,1个字节依然存不下。我们按之前的思路,将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,sdshdr16结构如下图所示:
在这里插入图片描述
其中“表头”共占用了S[2(len)+2(alloc)+1(flags)]个字节。flags的内容与sdshdr5类似,依然采用3位存储类型,但剩余5位不存储长度。

在Redis的源代码中,对类型的宏定义如下:

    #define SDS_TYPE_5   0
    #define SDS_TYPE_8   1
    #define SDS_TYPE_16 2
    #define SDS_TYPE_32 3
    #define SDS_TYPE_64 4

在Redis 5.0中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:

    struct __attribute__((__packed__))sdshdr8 {
        uint8_t len; /* 已使用长度,用1字节存储 */
        uint8_t alloc; /* 总长度,用1字节存储*/
        unsigned char flags; /* 低3位存储类型,高5位预留 */
        char buf[]; /*柔性数组,存放实际内容*/
    };
    struct __attribute__((__packed__))sdshdr16 {
        uint16_t len; /*已使用长度,用2字节存储*/
        uint16_t alloc; /* 总长度,用2字节存储*/
        unsigned char flags; /* 低3位存储类型,高5位预留 */
        char buf[]; /*柔性数组,存放实际内容*/
    };
    struct __attribute__((__packed__))sdshdr32 {
        uint32_t len; /*已使用长度,用4字节存储*/
        uint32_t alloc; /* 总长度,用4字节存储*/
        unsigned char flags; /* 低3位存储类型,高5位预留 */
        char buf[]; /*柔性数组,存放实际内容*/
    };
    struct __attribute__((__packed__))sdshdr64 {
        uint64_t len; /*已使用长度,用8字节存储*/
        uint64_t alloc; /* 总长度,用8字节存储*/
        unsigned char flags; /* 低3位存储类型,高5位预留 */
        char buf[]; /*柔性数组,存放实际内容*/
    };

可以看到,这4种结构的成员变量类似,唯一的区别是len和alloc的类型不同。结构体中4个字段的具体含义分别如下:

  • len:表示buf中已占用字节数。
  • alloc:表示buf中已分配字节数,不同于free,记录的是为buf分配的总长度。
  • flags:标识当前结构体的类型,低3位用作标识位,高5位预留。
  • buf:柔性数组,真正存储字符串的数据空间。

2、基本操作

数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。

2.1、创建字符串

Redis通过sdsnewlen函数创建SDS。在函数中会根据字符串长度选择合适的类型,初始化完相应的统计值后,返回指向字符串内容的指针,根据字符串长度选择不同的类型:

    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        char type = sdsReqType(initlen); //根据字符串长度选择不同的类型
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; //SDS_TYPE_5强制转化
            为SDS_TYPE_8
        int hdrlen = sdsHdrSize(type); //计算不同头部所需的长度
        unsigned char *fp; /* 指向flags的指针 */
        sh = s_malloc(hdrlen+initlen+1); //"+1"是为了结束符’\0'
        ...
        s = (char)sh+hdrlen; //s是指向buf的指针
        fp = ((unsigned char)s)-1; //s是柔性数组buf的指针,-1即指向flags
        ...
        s[initlen] = '\0'; //添加末尾的结束符
        return s;
    }

注意: Redis 3.2后的SDS结构由1种增至5种,且对于sdshdr5类型,在创建空字符串时会强制转换为sdshdr8。原因可能是创建空字符串后,其内容可能会频繁更新而引发扩容,故创建时直接创建为sdshdr8。

创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点:

  • 创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。
  • 长度计算时有“+1”操作,是为了算上结束符“ \0 ”。
  • 返回值是指向sds结构buf字段的指针。

返回值sds的类型定义如下:

    typedef char *sds;

从源码中我们可以看到,其实s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。

2.2、释放字符串

SDS提供了直接释放内存的方法——sdsfree,该方法通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存:

    void sdsfree(sds s) {
        if (s == NULL) return;
        s_free((char)s-sdsHdrSize(s[-1])); //此处直接释放内存
    }

为了优化性能(减少申请内存的开销), SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存:

    void sdsclear(sds s) {
        sdssetlen(s, 0); //统计值len归零
        s[0] = '\0'; //清空buf
    }

2.3、拼接字符串

拼接字符串操作本身不复杂,可用sdscatsds来实现,代码如下:

    sds sdscatsds(sds s, const sds t) {
        return sdscatlen(s, t, sdslen(t));
    }

sdscatsds是暴露给上层的方法,其最终调用的是sdscatlen。由于其中可能涉及SDS的扩容,sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。

    /* 将指针t的内容和指针s的内容拼接在一起,该操作是二进制安全的*/
    sds sdscatlen(sds s, const void *t, size_t len) {
        size_t curlen = sdslen(s);
        s = sdsMakeRoomFor(s, len);
        if (s == NULL) return NULL;
        memcpy(s+curlen, t, len); //直接拼接,保证了二进制安全
        sdssetlen(s, curlen+len);
        s[curlen+len] = '\0'; //加上结束符
        return s;
    }

下图描述了sdsMakeRoomFor的实现过程。
在这里插入图片描述
Redis的sds中有如下扩容策略:
1)若sds中剩余空闲长度avail大于新增内容的长度addlen,直接在柔性数组buf末尾追加即可,无须扩容。代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen)
    {
        void *sh, *newsh;
        size_t avail = sdsavail(s);
        size_t len, newlen;
        char type, oldtype = s[-1] & SDS_TYPE_MASK; //s[-1]即flags
        int hdrlen;
        if (avail >= addlen) return s; //无须扩容,直接返回
        ...
    }

2)若sds中剩余空闲长度avail小于或等于新增内容的长度addlen,则分情况讨论:新增后总长度len+addlen<1MB的,按新长度的2倍扩容;新增后总长度len+addlen>1MB的,按新长度加上1MB扩容。代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen)
    {
        ...
        newlen = (len+addlen);
        if (newlen < SDS_MAX_PREALLOC)// SDS_MAX_PREALLOC这个宏的值是1MB
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
        ...
    }

3)最后根据新长度重新选取存储类型,并分配空间。此处若无须更改类型,通过realloc扩大柔性数组即可;否则需要重新开辟内存,并将原字符串的buf内容移动到新位置。具体代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen)
    {
        ...
        type = sdsReqType(newlen);
        /* type5的结构不支持扩容,所以这里需要强制转成type8*/
        if (type == SDS_TYPE_5) type = SDS_TYPE_8;
        hdrlen = sdsHdrSize(type);
        if (oldtype==type) {
        /*无须更改类型,通过realloc扩大柔性数组即可,注意这里指向buf的指针s被更新了*/
                    newsh = s_realloc(sh, hdrlen+newlen+1);
            if (newsh == NULL) return NULL;
            s = (char)newsh+hdrlen;
        } else {
            /* 扩容后数据类型和头部长度发生了变化,此时不再进行realloc操作,而是直接重新开辟内存,
              拼接完内容后,释放旧指针*/
            newsh = s_malloc(hdrlen+newlen+1); //按新长度重新开辟内存
            if (newsh == NULL) return NULL;
            memcpy((char)newsh+hdrlen, s, len+1); //将原buf内容移动到新位置
            s_free(sh); //释放旧指针
            s = (char)newsh+hdrlen; //偏移sds结构的起始地址,得到字符串起始地址
            s[-1] = type; //为falgs赋值
            sdssetlen(s, len); //为len属性赋值
        }
        sdssetalloc(s, newlen); //为alloc属性赋值
        return s;
    }

2.4、其余API

下表列出了其他常用的API:
在这里插入图片描述
学习时把握以下两点:

  • SDS暴露给上层的是指向柔性数组buf的指针。
  • 读操作的复杂度多为O(1),直接读取成员变量;涉及修改的写操作,则可能会触发扩容。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis——简单动态字符串(Simple Dynamic Strings,SDS) 的相关文章

  • 如何在实时添加对象时从 Redis 中弹出对象?

    我想让 Node js 进程运行 因为它正在检查 Redis 服务器是否有任何新的弹出内容 另一个进程将偶尔进行推送 而 Node 进程将尝试弹出任何进来的内容 Node 进程将保持运行 有人能给我指出一个好的方向吗 我正在尝试找出如何监听
  • 连接到 localhost:6379 时出现错误 99。无法分配请求的地址

    设置 我有一个虚拟机 并在虚拟机中运行三个容器 一个 nginx 代理 一个非常简约的 Flask 应用程序和 redis Flask 应在端口 5000 上提供服务 而 redis 应在 6379 上提供服务 这些容器中的每一个都可以作为
  • Redis 写入 .ssh/authorized_keys

    当前设置 2 个主服务器 12 个工作服务器 工作人员通过 ssh copy id 连接到主设备 主设备和工作人员正在主设备上的 redis 队列中写入数据 过去一周我遇到的问题是 Redis 正在将数据写入authorized keys
  • 我的 Redis 自动生成的密钥

    我不知道我的 Redis 版本 4 0 9 到底发生了什么 我正在运行一个应用程序并使用 Redis 来存储我的数据库 但是 然后 Redis 自动创建 3 个新键 Backup1 Backup2 Backup3 并删除我的所有数据 这是我
  • 为什么Redis中不建议使用KEYS?

    在Redis中 建议不要使用按键命令 https redis io commands KEYS 为什么会这样呢 是因为它的时间复杂度是 O N 吗 或者是别的什么原因 我做了下面的实验来证明KEYS命令有多么危险 当带有 KEYS 的一个命
  • 如何统计 Redis 流中未读或已确认的消息?

    使用 Redis 5 0 3 假设我们创建一个名为streamy和一个消费群体consumers XGROUP CREATE streamy consumers MKSTREAM 然后向其中添加一些消息 XADD streamy messa
  • WSL Redis 遇到系统尚未使用 systemd 作为 init 系统(PID 1)启动。无法操作[已关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我正在尝试遵循本文中讨论的 Redis 安装过程article https www digitalocean com community
  • Docker-compose Predis 不通过 PHP 连接

    我正在尝试使用 docker compose 将 PHP 与 redis 连接 docker compose yml version 2 services redis image redis 3 2 2 php image company
  • Redis hash写入速度非常慢

    我面临一个非常奇怪的问题 使用 Redis 时 我的写入速度非常糟糕 在理想的情况下 写入速度应该接近 RAM 上的写入速度 这是我的基准 package redisbenchmark import redis clients jedis
  • Redis INCRBY 有限制

    我想知道是否有一种方法可以通过我的应用程序的单次往返在 Redis 中执行此操作 对于给定的键K 其可能值V是范围内的任意整数 A B 基本上 它有上限和下限 When an INCRBY or DECRBY发出命令 例如INCRBY ke
  • Spring Data Redis JedisConnectionException:流意外结束

    雷迪斯3 0 5Spring数据Redis 1 3 6绝地武士2 6 3 我们的 Web 应用程序通过 pub sub 从 Redis 接收数据 还以键 值对的形式在 Redis 上执行数据读 写 读 写发生在监听线程 独立监控线程和htt
  • 从redis中检索大数据集

    一台服务器上的应用程序查询另一台服务器上运行的 Redis 查询的结果数据集约为 250kzrangebyscore objects locations inf inf这在应用程序服务器上似乎需要 40 秒 当使用命令执行时redis cl
  • 如何将 ActionController::Live 与 Resque + Redis 一起使用(用于聊天应用程序)

    我正在尝试为我的 Rails 应用程序构建聊天功能 我在用ActionController Live Puma Resque Redis为了这 所以基本上在这种情况下 redissubscribe方法正在后台运行 使用resque 到目前为
  • 无法启动redis.service:单元redis-server.service被屏蔽

    我在 ubuntu 16 04 上安装了 Redis 服务器 但是当我尝试使用启动redis服务时 sudo systemctl start redis 我收到消息 Failed to start redis service Unit re
  • 如何设置和获取Redis中存储的对象?

    我试图在 redis 中存储一个对象 当我获取该对象时 它似乎不起作用 I tried u User new u name blankman redis set test u x redis get test x name error 我想
  • 如何在Redis中从hmset()切换到hset()?

    我收到弃用警告 即 Redis hmset 已弃用 请改用 Redis hset 但是 hset 采用第三个参数 我不知道是什么name应该是 info users 10 timestamp datetime utcnow strftime
  • 使用 Sentinels 升级 Redis 的最佳实践?

    我有 3 个 Redis 节点 由 3 个哨兵监视 我进行了搜索 文档似乎不清楚如何最好地升级此类配置 我目前使用的是 3 0 6 版本 我想升级到最新的 5 0 5 我对这方面的程序有几个疑问 升级两个大版本可以吗 我在我们的暂存环境中执
  • 2 个具有共享 Redis 依赖的 Helm Chart

    目前 我有 2 个 Helm Charts Chart A 和 Chart B Chart A 和 Chart B 对 Redis 实例具有相同的依赖关系 如Chart yaml file dependencies name redis v
  • redis - 使用哈希

    我正在使用 redis 为我的 Web 应用程序实现社交流和通知系统 我是 redis 的新手 我对哈希值及其效率有一些疑问 我读过这篇很棒的文章Instagram 帖子 http instagram engineering tumblr
  • Redis是如何实现高吞吐量和高性能的?

    我知道这是一个非常普遍的问题 但是 我想了解允许 Redis 或 MemCached Cassandra 等缓存 以惊人的性能极限工作的主要架构决策是什么 如何维持连接 连接是 TCP 还是 HTTP 我知道它完全是用C写的 内存是如何管理

随机推荐

  • 走进厦航,体验智能会计时代的业财融合

    12月1日 由用友网络与厦门国家会计学院共同组织的 业财融合高端管理人员培训班 正式开课 此次培训致力于通过产学研共创 服务企业财务管理创新及人才培养 助力企业落地业财融合 走向世界一流 下午 培训班全体学员共同走进中国唯一一家持续盈利36
  • js读取文件路劲并生成xlsx

    const xlsx require node xlsx const fs require fs const path require path fileDisplay url callback param url 你即将读取的文件夹路径
  • 二叉树先中后序遍历-12.4(day12)

    二叉树三种基本遍历方式 先序遍历 从根节点开始 逐级向下遍历 中序遍历 从左子树最后一层的最左侧节点开始遍历 当遍历到根节点后在开始遍历右子树 后续遍历 先访问叶子节点 从左子树到右子树 最后到根节点 记忆方法 先 中 后可以理解为前 中
  • ABB UNS3670A-Z V2 HIEE205011R000 电压转换器

    ABB UNS3670A Z V2 HIEE205011R000是一款电压转换器 通常用于电力系统和工业控制应用中 用于测量和转换电压信号 该电压转换器的主要特点包括 高精度 能够精确地测量和转换电压信号 确保电力系统稳定运行 高稳定性 具
  • 最新最全的Postman接口测试: postman实现参数化

    什么时候会用到参数化 比如 一个模块要用多组不同数据进行测试 验证业务的正确性 Login模块 正确的用户名 密码 成功 错误的用户名 正确的密码 失败 postman 实现参数化 在实际的 接口测试 中 部分参数每次发送请求时都要唯一 比
  • 网盘系统设计:万亿 GB 网盘如何实现秒传与限速?

    Java全能学习面试指南 https javaxiaobear cn 网盘 又称云盘 是提供文件托管和文件上传 下载服务的网站 File hostingservice 人们通过网盘保管自己拍摄的照片 视频 通过网盘和他人共享文件 已经成为了
  • 【有限窗口RLS算法】【自适应滤波器】与指数窗口和滑动窗口RLS算法相当的复杂度实现具有任意有限长度窗口的RLS算法研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码实现
  • Linux系统配置深度学习环境之cudnn安装

    前言 一个针对深度学习应用优化的 GPU 加速库 它提供了高性能 高可靠性的加速算法 旨在加速深度神经网络模型的训练和推理过程 cuDNN 提供了一系列优化的基本算法和函数 包括卷积 池化 规范化 激活函数等 以及针对深度学习任务的高级功能
  • ABB CI858K01 3BSE018135R1 通讯接口

    ABB CI858K01 3BSE018135R1 通讯接口 产品详情 ABB CI858K01 3BSE018135R1的通讯接口有以下特点 多种通信接口 CI858K01接口模块可能支持多种通信接口 例如以太网 串口 CAN总线等 用于
  • E (1052) : DS树--带权路径和

    文章目录 一 题目描述 二 输入与输出 1 输入 2 输出 三 参考代码 一 题目描述 计算一棵二叉树的带权路径总和 即求赫夫曼树的带权路径和 已知一棵二叉树的叶子权值 该二叉树的带权路径和APL等于叶子权值乘以根节点到叶子的分支数 然后求
  • mixamo根动画导入UE5问题:滑铲

    最近想做一个跑酷游戏 从mixamo下载滑铲动作后 出了很多动画的问题 花了两周时间 终于是把所有的问题基本上都解决了 常见问题 1 动画序列 人物不移动 2 动画序列 人物移动朝向错误 3 蒙太奇 人物移动后会被拉回 4 蒙太奇 动画移动
  • Java架构师技术为业务赋能

    目录 1 概论 2 天猫的难言之隐 3 如何拆解技术难点 三段论 4 天猫线的破局之道 双引擎回归测试框架 5 架构师的心理游戏 解决问题从转换思维开始 6 技术助力业务的两个方向 7 阿里新零售部门如何培养技术团队的业务知识 8 如何围绕
  • acwing算法提高之动态规划--数字三角形模型

    目录 1 基础知识 2 模板 3 工程化 1 基础知识 暂无 2 模板 暂无 3 工程化 题目1 摘花生 解题思路 DP 状态定义 f i j 从 1 1 走到 i j 所摘花生总和 状态转移 有 从上方走到 i j 有 f i 1 j w
  • 服务器入侵如何防护,业务被攻击如何处理,服务器安全防护方案

    服务器是算是家用电脑的一种使用方法 主机不在用户家中 需要远程使用 在目前互联网时代占用很重要的位置 当然生活中也是应用广泛 服务器比普通计算机运行更快 负载更高 价格更贵 很多娱乐 工作都需要依靠服务器来运行整个体系 因此服务器的安全防护
  • 十分钟带你看懂——Python测试框架之pytest最全讲

    pytest特短 pytest是一个非常成熟的全功能的Python测试框架 主要有以下几个特点 简单灵活 容易上手 支持参数化 能够支持简单的单元测试和复杂的功能测试 还可以用来做selenium appnium等自动化测试 接口自动化测试
  • Python安装步骤介绍

    本文将介绍Python安装的详细步骤如下 下载 python 安装 python 配置环境变量 安装时勾选配置环境变量的则无需此步骤 一 python下载 官网 Download Python Python org 根据电脑位数下载所需的版
  • ICS TRIPLEX T9110高级处理器模块

    ICS TRIPLEX T9110高级处理器模块是一款高性能的处理器模块 通常用于工业控制和自动化系统中 它采用最新的Intel芯片组技术 包括815E芯片组 具有以下特点 高性能 T9110模块通常配备高性能的处理器 能够处理复杂的控制算
  • Spring到底是如何解决循环依赖问题的?

    Spring作为当前使用最广泛的框架之一 其重要性不言而喻 所以充分理解Spring的底层实现原理对于咱们Java程序员来说至关重要 那么今天笔者就详细说说Spring框架中一个核心技术点 如何解决循环依赖问题 什么是循环依赖问题 Spri
  • node express 批量压缩图片

    场景 我是10万张图片 只压缩 png jpg jpeg 分批次压缩 因为压缩要内存限制 不能一次性压缩那么多 const fs require fs const path require path const images require
  • Redis——简单动态字符串(Simple Dynamic Strings,SDS)

    简单动态字符串 Simple Dynamic Strings SDS 是Redis的基本数据结构之一 用于存储字符串和整型数据 SDS兼容C语言标准字符串处理函数 且在此基础上保证了二进制安全 1 数据结构 在了解SDS源码前 我们先思考一