[redis 源码走读] sds

2024-03-19 05:10
文章标签 源码 redis sds 走读

本文主要是介绍[redis 源码走读] sds,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

数据结构

为了节省空间,增加内存的利用率,struct 数据结构没有进行内存对齐,redis 的瓶颈不在 cpu 而在内存。同时,为了灵活处理不同长度范围的字符串,redis 定义了下面几种数据结构。

typedef char *sds;
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));/* Note: sdshdr5 is never used, we just access the flags byte directly.* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {// 当字符串很小时, `flags` 是一个8 个字节的组合字符,前 3 bit 是字符串类型,后面5bit是字符串长度。unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* used */uint8_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* used */uint16_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
成员描述
len当前已使用的内存空间长度
alloc分配的内存空间长度
flags数据结构类型 或者 (数据结构类型 + 字符串长度 例如:sdshdr5)
bufuf 如果有数据,是以 ‘\0’ 结束的字符串。

数据结构视图

数据结构内存

制作图表方法可以用 processon,参考视频

  • bilibili: 绘制 redis sds 数据结构内存空间视图
  • youtube: Draw Redis SDS Struct Memmory Chart

结构大小

可以通过函数 sdsReqType 知道,sds 数据结构,是根据数据长度范围去确定数据结构类型的。下面列出的数据结构的比较。

结构类型大小字符串长度
sdshdr511 << 5 - 1
sdshdr831 << 8 - 1
sdshdr1651 << 16 - 1
sdshdr3291 << 32 - 1
sdshdr6417大于 1 << 32
static inline char sdsReqType(size_t string_size) {if (string_size < 1<<5)// 1 << 5 == 32,所以长度最大 31,二进制 11111,占 5 位。结合数据结构可以查看 flags 的组合,左移 5 位,存储字符串长度,右边3位存储字符串长度。return SDS_TYPE_5;if (string_size < 1<<8)return SDS_TYPE_8;if (string_size < 1<<16)return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)if (string_size < 1ll<<32)return SDS_TYPE_32;return SDS_TYPE_64;
#elsereturn SDS_TYPE_32;
#endif
}
  • 例如 sdshdr32 数据结构, sizeof(sdshdr32) == 9 ,如果是字节对齐,应该 12 才对。
struct __attribute__((__packed__)) sdshdr32 {uint32_t len;        /* used */uint32_t alloc;      /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};

核心接口

sds 主要的逻辑是对字符串内存管理。可以参考下面接口进行理解。

接口描述
sdsnew创建字符串对象
sdsfree释放字符串结构对象
sdsavail查询字符串对象空闲内存大小
sdsnewlen根据字符串长度,分配合适的内存空间,设置数据结构的相关的成员数据
sdsMakeRoomFor为对象分配增长的空间,增长小于 1M, newlen *= 2,否则 newlen += SDS_MAX_PREALLOC

工作流程

我们依旧可以用 gdb 对 sds 进行调试,熟悉它对工作流程。作者在 sds.c 文件就设置了测试宏SDS_TEST_MAIN,我们可以编译一个文件进行调试。

gcc -g  -DSDS_TEST_MAIN sds.c zmalloc.c -o sds

调试方法,可以参考视频

  • bilibili: Debug Redis sds with Gdb
  • youtube: Debug Redis sds with Gdb
  • 堆栈信息
#0  sdsnewlen (init=0x100006a71, initlen=3) at sds.c:99
#1  0x00000001000018a6 in sdsnew (init=0x100006a71 "foo") at sds.c:156
#2  0x0000000100004cb7 in sdsTest () at sds.c:1130
#3  0x0000000100006124 in main () at sds.c:1294
  • sdsnewlen 根据字符串长度,用不同数据结构进行存储,每个数据结构有不同类型。
/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {size_t initlen = (init == NULL) ? 0 : strlen(init);return sdsnewlen(init, initlen);
}
  • 根据字符串长度,分配合适的内存空间,设置数据结构的相关的成员数据
/* Create a new sds string with the content specified by the 'init' pointer* and 'initlen'.* If NULL is used for 'init' the string is initialized with zero bytes.* If SDS_NOINIT is used, the buffer is left uninitialized;** The string is always null-termined (all the sds strings are, always) so* even if you create an sds string with:** mystring = sdsnewlen("abc",3);** You can print the string with printf() as there is an implicit \0 at the* end of the string. However the string is binary safe and can contain* \0 characters in the middle, as the length is stored in the sds header. */
sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;char type = sdsReqType(initlen);/* Empty strings are usually created in order to append. Use type 8* since type 5 is not good at this. */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. */// 申请数据结构内存。+ 1 是为了字符串的结束符 '\0'。sh = s_malloc(hdrlen+initlen+1);if (init==SDS_NOINIT)init = NULL;else if (!init)memset(sh, 0, hdrlen+initlen+1);if (sh == NULL) return NULL;s = (char*)sh+hdrlen;fp = ((unsigned char*)s)-1;switch(type) {case SDS_TYPE_5: {// SDS_TYPE_BITS *fp = type | (initlen << SDS_TYPE_BITS);break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}}if (initlen && init)memcpy(s, init, initlen);s[initlen] = '\0';return s;
}
  • 获取字符串长度
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)static inline size_t sdslen(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:// 一个字节高 5 位是长度,通过向右移动 3 位获得大小。return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8: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;
}
  • 释放内存,因为 sds struct 是一个连续的内存数据结构,根据 sds 指向的 buf,往回找 struct 的起始地址,进行释放。

看看 sdsnewlen 是如何申请内存的。

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {if (s == NULL) return;s_free((char*)s-sdsHdrSize(s[-1]));
}
static inline int sdsHdrSize(char type) {switch(type&SDS_TYPE_MASK) {case SDS_TYPE_5:return sizeof(struct sdshdr5);case SDS_TYPE_8:return sizeof(struct sdshdr8);case SDS_TYPE_16:return sizeof(struct sdshdr16);case SDS_TYPE_32:return sizeof(struct sdshdr32);case SDS_TYPE_64:return sizeof(struct sdshdr64);}return 0;
}
  • 查询数据结构多少空闲内存空间
static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {// 小于 32 长度的内存,都是直接申请的,没有空余内存。return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);return sh->alloc - sh->len;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);return sh->alloc - sh->len;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);return sh->alloc - sh->len;}}return 0;
}
  • 追加内存
/* Append the specified null termianted C string to the sds string 's'.** After the call, the passed sds string is no longer valid and all the* references must be substituted with the new pointer returned by the call. */
sds sdscat(sds s, const char *t) {return sdscatlen(s, t, strlen(t));
}
  • redis sds 习惯先根据长度,分配合适的内存,再进行数据拷贝等操作。
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the* end of the specified sds string 's'.** After the call, the passed sds string is no longer valid and all the* references must be substituted with the new pointer returned by the call. */
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;
}
  • 根据增长的长度,为 sds 申请合适长度的空间。
/* Enlarge the free space at the end of the sds string so that the caller* is sure that after calling this function can overwrite up to addlen* bytes after the end of the string, plus one more byte for nul term.** Note: this does not change the *length* of the sds string as returned* by sdslen(), but only the free buffer space we have. */
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;int hdrlen;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);// 小于 1 M 内存的翻倍增加,否则每次增加 1Mif (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;type = sdsReqType(newlen);// 如果小数据,遇到 cat 操作,类型升级到 SDS_TYPE_8,方便 cat 的后续操作。这里作者估计是根据很多场景结合的经验得出的结论。/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;// 根据对应类型的对象申请相应的空间。hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */newsh = s_malloc(hdrlen+newlen+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, newlen);return s;
}
  • 空数据结构 sdsempty(),一些不定长的字符串,例如 sdscatprintf,格式化的字符串,经常性有很长的字符串。所以在 sdsnewlen 中给申请 SDS_TYPE_8 类型进行处理。
sds sdsnewlen(const void *init, size_t initlen) {if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
}
  • 去掉字符串头尾出现在字串的所有字符
/* Remove the part of the string from left and from right composed just of* contiguous characters found in 'cset', that is a null terminted C string.** After the call, the modified sds string is no longer valid and all the* references must be substituted with the new pointer returned by the call.** Example:** s = sdsnew("AA...AA.a.aa.aHelloWorld     :::");* s = sdstrim(s,"Aa. :");* printf("%s\n", s);** Output will be just "HelloWorld".*/
sds sdstrim(sds s, const char *cset) {char *start, *end, *sp, *ep;size_t len;// 通过两次遍历,从头向尾,sp = start = s;ep = end = s+sdslen(s)-1;while(sp <= end && strchr(cset, *sp)) sp++;while(ep > sp && strchr(cset, *ep)) ep--;len = (sp > ep) ? 0 : ((ep-sp)+1);if (s != sp) memmove(s, sp, len);s[len] = '\0';sdssetlen(s,len);return s;
}
  • 两个 sds 字符串比较

取最短字符串的长度,该长度的两个字符串相比较,在这个条件基础上,对余下字符串进行比较。返回相应结果。

/* Compare two sds strings s1 and s2 with memcmp().** Return value:**     positive if s1 > s2.*     negative if s1 < s2.*     0 if s1 and s2 are exactly the same binary string.** If two strings share exactly the same prefix, but one of the two has* additional characters, the longer string is considered to be greater than* the smaller one. */
int sdscmp(const sds s1, const sds s2) {size_t l1, l2, minlen;int cmp;l1 = sdslen(s1);l2 = sdslen(s2);minlen = (l1 < l2) ? l1 : l2;cmp = memcmp(s1, s2, minlen);if (cmp == 0) return l1 > l2 ? 1 : (l1 < l2 ? -1 : 0);return cmp;
}

后记

源码走读系列,通过调试手段,走读源码,是自己流水账式的记录,从而理解了更多的实现细节。

这篇关于[redis 源码走读] sds的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/824895

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

Redis中使用布隆过滤器解决缓存穿透问题

一、缓存穿透(失效)问题 缓存穿透是指查询一个一定不存在的数据,由于缓存中没有命中,会去数据库中查询,而数据库中也没有该数据,并且每次查询都不会命中缓存,从而每次请求都直接打到了数据库上,这会给数据库带来巨大压力。 二、布隆过滤器原理 布隆过滤器(Bloom Filter)是一种空间效率很高的随机数据结构,它利用多个不同的哈希函数将一个元素映射到一个位数组中的多个位置,并将这些位置的值置

red5-server源码

red5-server源码:https://github.com/Red5/red5-server