Loading... ## string ### 观其面  kv结构,最大长度512M,底层数据结构为int和sds(简单动态字符串) * sds可以保存text数据和bin数据 使用len属性的值判断字符串是否结束,所有api都会以二进制形式处理sds存放在buf[]中的数据 * 采用len属性记录字符串长度,复杂度为O(1) * sds api安全,append不会造成bof 字符串对象的内部encoding有3种 * 如果是整数值,且能用long表示,那么对象会将整数值保存在ptr种,并将void*转会为long,设置encoding为int * 当类型是string的时候得分两种情况  其实吧,从这里就可以看出Redis对于字符字符串的管理还是挺不错的,你量少?行,那么给你分配连续的空间直接管理,量多?纳闷从新哪一个空间来管理。 好处可想而知: * `embstr`encoding将创建字符串对象所需的内存分配次数从 `raw` encoding的两次降低为一次; * 释放 `embstr`encoding的字符串对象同样只需要调用一次内存释放函数; * 因为`embstr`encoding的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。 但是embstr也有缺点: * 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以 **embstr-encoding的字符串对象实际上是只读的** ,redis没有为embstrencoding的字符串对象编写任何相应的修改程序。当我们对embstrencoding的字符串对象执行任何修改命令(例如append)时,程序会先将对象的encoding从embstr转换成raw,然后再执行修改命令。 ### 应用场景 * 缓存对象 * 计数 * ~分布式锁~ * 共享Session 这里分布式锁不太建议用string来实现,虽然Redis在1.6之后支持了setnx原子操作,不需要使用Lua脚本,但是任然没有解决可重入性问题,具体的解决方案使用Map,感兴趣的话可以看下我之前的文章:[Redis分布式锁深入分析 – Karos (wzl1.top)](https://www.wzl1.top/2023/06/redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90/) ### 究其身 下面是RedisObject数据结构   这里LRU和LFU是啥?补补os吧,上链接:[操作系统-超20000字的“总结” – Karos (wzl1.top)](https://www.wzl1.top/2023/02/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-%E8%B6%8520000%E5%AD%97%E7%9A%84%E6%80%BB%E7%BB%93/) type:4是啥?这里表示对象类型,后面的4是位域(这里还是做下补充吧[C 位域 | 菜鸟教程 (runoob.com)](https://www.runoob.com/cprogramming/c-bit-fields.html)) 在说sds之前,我们先来讨论一下C语言字符串的缺点吧: * 获取字符串长度的时间复杂度为 O(N); * 字符串的结尾是以 “\0” 字符标识,字符串里面不能包含有 “\0” 字符,因此不能保存二进制数据; * 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止; 前两点,不说,就说最后一个吧,虽然能够接受,还是要解释一下。 在C语言中,对字符串的各个操作都要通过函数进行,并且每个可修改字符串在定义的时候就已经固定了大小(感觉说的有点问题,好久没玩儿C了,一直用的都是C++的string,hhh~) 举个常见的例子,字符串拼接函数 <pre><div class="hljs"><code class="lang-c hljs">char *strcat(char *dest, constchar* src); </code></div></pre> 如果dest预留的长度小于src的长度,那么很有可能产生overflow,那么Redis的增强我们来看看吧  现在来对sds数据结构仔细说说吧,仔细看其实就是那几个玩意儿: * **len,记录了字符串长度** 。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。 * **alloc,分配给字符数组的空间长度** 。这样在修改字符串的时候,可以通过 `alloc - len` 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。 * **flags,用来表示不同类型的 SDS** 。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。 * **buf[],字符数组,用来保存实际数据** 。不仅可以保存字符串,也可以保存二进制数据。 因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 “\0” 字符。因此, SDS 的 API 都是 **以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的** 。通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。 #### 节省空间 SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。 Redis 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。 这 5 种类型的主要 **区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同** 。 为什么这样设计? 主要是为了 **能灵活保存不同大小的字符串,从而有效节省内存空间** 。比如,在保存小字符串时,结构头占用空间也比较少。 > 冷知识,这里还用了 `__attribute__ ((packed))`取消结构体在**编译过程**中的优化对齐,按照实际占用字节数进行对齐来进行优化。 #### 扩容机制 <pre><div class="hljs"><code class="lang-c hljs">/* Enlarge the free space at the end of the hisds 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 hisds string as returned * by hi_sdslen(), but only the free buffer space we have. */ hisds hi_sdsMakeRoomFor(hisds s, size_t addlen){ // 传入一个sds的char数组和需要增加的长度 void *sh, *newsh; size_t avail = hi_sdsavail(s); // 获取剩余空间 size_t len, newlen; char type, oldtype = s[-1] & HI_SDS_TYPE_MASK; int hdrlen; /* Return ASAP if there is enough space left. */ if (avail >= addlen) return s; // 剩余空间足够,无需扩展,直接返回 len = hi_sdslen(s); // 获取当前sds长度 sh = (char*)s-hi_sdsHdrSize(oldtype); newlen = (len+addlen); // 计算新的长度 if (newlen < HI_SDS_MAX_PREALLOC) // 动态扩容 HI_SDS_MAX_PREALLOC = 1MB newlen *= 2; else newlen += HI_SDS_MAX_PREALLOC; type = hi_sdsReqType(newlen); // 重新获取SDS类型 /* Don't use type 5: the user is appending to the string and type 5 is * not able to remember empty space, so hi_sdsMakeRoomFor() must be called * at every appending operation. */ if (type == HI_SDS_TYPE_5) type = HI_SDS_TYPE_8; // 扩容了,那么按照上文节约空间的原则,这里也要修改空间 hdrlen = hi_sdsHdrSize(type); if (oldtype==type) { newsh = hi_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 = hi_s_malloc(hdrlen+newlen+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen, s, len+1); hi_s_free(sh); s = (char*)newsh+hdrlen; s[-1] = type; hi_sdssetlen(s, len); } hi_sdssetalloc(s, newlen); return s; }</code></div></pre> 最后修改:2024 年 08 月 08 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏