|
|
1.前言Hello,接待大师来到《 Redis 数据结构源码剖析系列》,在《Redis为什么这么快?》一文中我说过 Redis 速度快的一个缘由就是其简单且高效的数据结构他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
本系列文章面向各个阶段的 Coder 们,新手也不用怕他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。每一篇文章敖丙都将从号令实战入门动手,随后深入源码剖析,最初口试题回首这三个偏向上给列位卷王逐一先容他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
2.SDS号令实战[初来乍到]SDS 是 Redis 中最简单的数据结构他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。Redis 中一切的数据结构都是以唯一的 key 字符串作为称号,按照 key 获得value,差别仅在于 value 的数据结构分歧他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
SDS 在生产情况中利用很是普遍,比如,我们利用 SDS 做散布式锁;将工具转成 JSON 串作为缓存等他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。在 Redis 口试进程中一旦说起相关数据结构 SDS 一定是绕不外去的话题,它很简单(大概说看完此文后很简单),口试官可以不问,但我们不能不懂他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
首先我们从号令实战起头切入吧~(老司机间接跳过)
更多号令检察官网:https://redis.io/commands#string
2. 1设备字符串格式:set 他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。其中value的值可以为字节串(byte string)、整型和浮点数他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
> set name aobingOK
2.2 获得字符串格式:get 他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
> get name"aobing"
2.3 获得字符串长度格式:strlen
> strlen name(integer) 6
2.4 获得子串格式:getrange start end他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。 获得字符串的子串,在Redis2.0之前此号令为substr,现利用getrange他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。返回位移为start(从0起头)和end之间(都包括,而不是像其他说话中的包头不包尾)的子串他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。可以利用负偏移量来供给从字符串末端起头的偏移量他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
是以-1暗示最初一个字符,-2暗示倒数第二个,依此类推他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。该函数经过将成果范围限制为字符串的现实长度来处置超越范围的请求(end设备很是大也是到字符串末端就停止了)他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
127.0.0.1:6379> set mykey "This is a string"OK127.0.0.1:6379> getrange mykey 0 3"This"127.0.0.1:6379> getrange mykey -3 -1"ing"127.0.0.1:6379> getrange mykey 0 -1"This is a string"127.0.0.1:6379> getrange mykey 10 10000"string"
2.5 设备子串格式:setrange offset substr他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。 返回值:点窜后字符串的长度他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
从value的全部长度起头,从指定的偏移量覆盖key处存储的一部分字符串他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。假如偏移量大于key处字符串确当前长度,则该字符串将添补零字节以使偏移量合适他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
不存在的键被视为空字符串,是以此号令将确保它包括充足大的字符串以可以将值设备为offset他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
留意:您可以设备的最大偏移为2^29 - 1(536870911),由于Redis字符串限制为512 MB他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。假如您需要超越此巨细,可以利用多个键他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
127.0.0.1:6379> set key1 "hello world"OK127.0.0.1:6379> setrange key1 6 redis(integer) 11127.0.0.1:6379> get key1"hello redis"127.0.0.1:6379> setrange key2 6 redis(integer) 11127.0.0.1:6379> get key2"x00x00x00x00x00x00redis"
2.6 追加子串格式:append substr 假如key已经存在而且是字符串,则此号令将value在字符串末端附加他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。假如key不存在,则会建立它并将其设备为空字符串,是以APPEND在这类特别情况下 将类似于SET他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
127.0.0.1:6379> exists key4(integer) 0127.0.0.1:6379> append key4 hello(integer) 5127.0.0.1:6379> append key4 world(integer) 10127.0.0.1:6379> get key4"helloworld"
2.7 计数在利用Redis中我们经常将字符串做为计数器,利用incr号令停止加一他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。 格式:incr 他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。 返回值:key递增后的值他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。 将存储的数字key加1他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
假如key不存在,则在履行操纵之前将其设备为0他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
假如key包括毛病范例的值或包括不能暗示为整数的字符串,则返回毛病他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。此操纵仅限于64位带标记整数他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。计数是由范围的,它不能跨越Long.Max,不能低于Long.Min他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
2.8 过期和删除字符串可以利用del号令停止删除,也可以利用expire号令设备过期时候,到期自动删除他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。我们可以利用ttl号令获得字符串的寿命(还有几多时候过期)他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
格式:del ... 返回值:删除key的个数
127.0.0.1:6379> SET key1 "Hello""OK"127.0.0.1:6379> SET key2 "World""OK"127.0.0.1:6379> DEL key1 key2 key3(integer) 2
格式:expire time 返回值:假如设备了超时返回1他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。假如key不存在返回0他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
若何将设备了过期的字符串设备为永久的呢?
保存时候可以经过利用DEL号令来删除全部 key 来移除,大概被SET和GETSET号令覆写(overwrite),这意味着,假如一个号令只是点窜一个带保存时候的 key 的值而不是用一个新的 key 值来取代(replace)它的话,那末保存时候不会被改变他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
比如说,对一个 key 履行INCR号令,对一个列表停止LPUSH号令,大概对一个哈希表履行HSET号令,这类操纵都不会点窜 key 自己的保存时候他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
假如利用RENAME对一个 key 停止更名,那末更名后的 key 的保存时候和更名前一样他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
RENAME 号令的另一种能够是,尝试将一个带保存时候的 key 更名成另一个带保存时候的 another_key ,这时旧的 another_key (以及它的保存时候)会被删除,然后旧的 key 会更名为 another_key ,是以,新的 another_key 的保存时候也和原本的 key 一样他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
利用PERSIST号令可以在不删除 key 的情况下,移除 key 的保存时候,让 key 重新成为一个『持久的』(persistent) key 他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
127.0.0.1:6379> expire age 100(integer) 1127.0.0.1:6379> ttl age(integer) 97127.0.0.1:6379> set age 20OK127.0.0.1:6379> ttl age(integer) -1127.0.0.1:6379> expire age 100(integer) 1127.0.0.1:6379> ttl age(integer) 98127.0.0.1:6379> rename age age2OK127.0.0.1:6379> ttl age2(integer) 87127.0.0.1:6379> expire age 100(integer) 1127.0.0.1:6379> ttl age(integer) 96127.0.0.1:6379> persist age(integer) 1127.0.0.1:6379> ttl age(integer) -1
3.SDS 简介与特征[八股]Redis 口试中大要率会说起相关的数据结构,SDS 的八股文大部分人滚瓜烂熟,可是没有读过源码,知其然不知其所以然,这可万万使不得呀!!
4.SDS 结构模子[老司机]本次敖丙阅读的Redis源码为最新的 Redis6.2.6 和 Redis3.0.0 版本
相信列位看官在听到 Redis 中的字符串不是简简单单的C说话中的字符串,是 SDS(Simple Dynamic String,简单静态字符串)时以为是造出了啥新范例呢,对此,敖丙想说的是不慌,实在 SDS 内容的源码底层就是typedef char *sds;他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
4.1 数据结构Redis6.x 的 SDS 的数据结构界说与 Redis3.0.0 相差比力大,可是焦点思惟稳定他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。先从简单版本(Redis3.x)起头吧~
struct sdshdr {//记录buf数组中已利用字节的数目//即是SDS所保存字符串的长度unsigned int len;//记录buf数组中未利用字节的数目unsigned int free;//char数组,用于保存字符串char buf[];};
以下图所示为字符串"Aobing"在Redis中的存储形式:
len 为6,暗示这个 SDS 保存了一个长度为5的字符串;free 为0,暗示这个 SDS 没有残剩空间;buf 是个char范例的数组,留意末端保存了一个空字符'0'他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。buf 尾部自动追加一个'0'字符并不会计较在 SDS 的len中,这是为了遵守 C 字符串以空字符串结尾的老例,使得 SDS 可以间接利用一部分string.h库中的函数,如strlen
#include #include int main(){char buf[] = {'A','o','b','i','n','g','0'};printf("%sn",buf); // Aobingprintf("%lun",strlen(buf)); // 6return 0;}
4.2 刻薄的数据优化4.2.1 数据结构优化今朝我们似乎获得了一个结构不错的 SDS ,可是我们能否继续停止优化呢?
在 Redis3.x 版本中分歧长度的字符串占用的头部是不异的,假如某一字符串很短可是头部却占用了更多的空间,这不免太浪费了他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。所以我们将 SDS 分为三种级此外字符串:
短字符串(长度小于32),len和free的长度用1字节即可;长字符串,用2字节大概4字节;超长字符串,用8字节他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。共有五品种型的SDS(长度小于1字节、1字节、2字节、4字节、8字节)
我们可以在 SDS 中新增一个 type 字段来标识范例,可是没需要利用一个 4 字节的int范例去做!可以利用 1 字节的char范例,经过位运算(3位即可标识2^3品种型)来获得范例他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
以下所示为短字符串(长度小于32)的优化形式:
低三位存储范例,高5位存储长度,最多能标识的长度为32,所以短字符串的长度一定小于32他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
无需free字段了,32-len即为free
敖丙带大师分析了一波,接下来看看Redis6.x中是怎样做的吧!
// 留意:sdshdr5从未被利用,Redis中只是拜候flags他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; char buf[];};struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; uint8_t alloc; unsigned char flags; char buf[];};struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; uint16_t alloc; unsigned char flags; char buf[];};struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; uint32_t alloc; unsigned char flags; char buf[];};struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; uint64_t alloc; unsigned char flags; char buf[];};
数据结构和我们分析的差不多嘛!也是加一个标识字段而已,而且不是int范例,而是1字节的char范例,利用其中的3位暗示具体的范例他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
同时,Redis 中也声了然5个常量别离暗示五品种型的 SDS ,与我们分析的也不约而合他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
#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
4.2.2 uintX_t对照前后两版代码,不难发现在 Redis6.x 中 int 范例也多出了几种:uint8_t / uint16_t / uint32_t /uint64_t他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。乍一看以为是新增范例呢,究竟 C说话里面可没有这些范例呀!
敖丙初见也是满头雾水,究竟C 说话忘得差不多了他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。不外我凭仗强大的常识储备(不要face ^_^)猜测这能够是一个体名,C说话中有typedef呀!而_t就是其缩写他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。检察相关源码,公然~~
typedef unsigned char uint8_t;typedef unsigned short uint16_t;typedef unsigned int uint32_t;typedef unsigned long long uint64_t;
4.2.3 对齐添补在 Redis6.x 的源码中 SDS 的结构体为struct __attribute__ ((__packed__))与struct有较大的不同,这实在和我们熟知的对齐添补有关他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
(1) 举个栗子斟酌以下结构体:
typedef struct{char c1;short s;char c2;int i;} s;
若此结构体中的成员都是松散排列的,假定c1的肇端地址为0,则s的地址为1,c2的地址为3,i的地址为4他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。下面用代码论证一下我们的假定他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
#include typedef struct{char c1;short s;char c2;int i;} s;int main(){s a;printf("c1 -> %d, s -> %d, c2 -> %d, i -> %dn",(unsigned int)(void *)&a.c1 - (unsigned int)(void *)&a,(unsigned int)(void *)&a.s - (unsigned int)(void *)&a,(unsigned int)(void *)&a.c2 - (unsigned int)(void *)&a,(unsigned int)(void *)&a.i - (unsigned int)(void *)&a);return 0;}// 成果为:c1 -> 0, s -> 2, c2 -> 4, i -> 8
为难了,和假定差的不是一星半点呀!这就是对齐添补搞的鬼,这啥啥啥呀~
(2) 什么是字节对齐现代计较机中,内存空间依照字节分别,理论上可以从任何肇端地址拜候肆意范例的变量他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。但现实中在拜候特定范例变量时经常在特定的内存地址拜候,这就需要各类范例数据依照一定的法则在空间上排列,而不是顺序一个接一个地寄存,这就是对齐他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
(3) 对齐缘由为什么需要对齐添补是由于各个硬件平台对存储空间的处置上有很大的分歧他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。一些平台对某些特定范例的数据只能从某些特定地址起头存取他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
最多见的是假如不依照合适其平台的要求对数据寄存停止对齐,会在存取效力上带来损失他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
比若有些平台每次读都是从偶地址起头,假如一个int型(假定为 32位)寄存在偶地址起头的地方,那末一个读周期便可以读出,而假如寄存在奇地址起头的地方,便能够会需要2个读周期,并对两次读出的成果的凹凸字节停止拼集才能获得该int数据,致使在读取效力高低降很多他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
(4) 变动对齐方式留意:我们写法式的时辰,不需要斟酌对齐题目他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。编译器会替我们挑选合适方针平台的对齐战略他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
假如我们一定要手动变动对齐方式,一般可以经过下面的方式来改变缺省的对界条件:
利用伪指令#pragma pack(n):C编译器将依照n个字节对齐;利用伪指令#pragma pack(): 取消自界说字节对齐方式他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。 别的,还有以下的一种方式(GCC特有语法):__attribute((aligned (n))): 让所感化的结组成员对齐在n字节自然鸿沟上他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。假如结构体中有成员的长度大于n,则依照最大成员的长度来对齐他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。__attribute__ ((packed)): 取消结构在编译进程中的优化对齐,依照现实占用字节数停止对齐他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。将上述示例代码的结构体变动以下(取消对齐),再次履行,可以发现取消对齐后和我们的假定就分歧了他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
typedef struct __attribute__ ((__packed__)){char c1;short s;char c2;int i;} s;// 再次履行:c1 -> 0, s -> 1, c2 -> 3, i -> 4
(5) Redis为什么差池齐呢?综上所述我们晓得了对齐添补可以进步 CPU 的数据读取效力,作为 IO 频仍的 Redis 为什么挑选差池齐呢?
我们再次回首 Redis6.x 中的 SDS 结构:
有个细节列位需要晓得,即 SDS 的指针并不是指向 SDS 的肇端位置(len位置),而是间接指向buf[],使得 SDS 可以间接利用 C 说话string.h库中的某些函数,做到了兼容,非常nice~他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
假如不停止对齐添补,那末在获得当前 SDS 的范例时则只需要前进一步即可flagsPointer = ((unsigned char*)s)-1;
相反,若停止对齐添补,由于 Padding 的存在,我们在分歧的系统中不晓得退几多才能获得flags,而且我们也不能将 sds 的指针指向flags,这样就没法兼容 C 说话的函数了,也不晓得进步几多才能获得 buf[]他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
4.3 SDS 上风
4.3.1 O(1)时候复杂度获得字符串长度由于C字符串不记录本身的长度,所以为了获得一个字符串的长度法式必须遍历这个字符串,直至碰到'0'为止,全部操纵的时候复杂度为O(N)他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。而我们利用SDS封装字符串则间接获得len属性值即可,时候复杂度为O(1)他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
4.3.2 二进制平安什么是二进制平安?
浅显地讲,C说话中,用'0'暗示字符串的竣事,假如字符串自己就有'0'字符,字符串就会被截断,即非二进制平安;若经过某种机制,保证读写字符串时不侵害其内容,则是二进制平安他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
C字符串中的字符除了末端字符为'0'外其他字符不能为空字符,否则会被以为是字符串结尾(即使现实上不是)他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。这限制了C字符串只能保存文本数据,而不能保存二进制数据他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。而SDS利用len属性的值判定字符串能否竣事,所以不会受'0'的影响他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
4.3.3 根绝缓冲区溢出字符串的拼接操纵是利用非常频仍的,在C说话开辟中利用char *strcat(char *dest,const char *src)方式将src字符串中的内容拼接到dest字符串的末端他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
由于C字符串不记录本身的长度,一切strcat方式已经以为用户在履行此函数时已经为dest分派了充足多的内存,足以包容src字符串中的一切内容,而一旦这个条件不建立就会发生缓冲区溢出,会把其他数据覆盖掉,Dangerous~他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
// strcat 源码char * __cdecl strcat (char * dst, const char * src){char * cp = dst;while( *cp )cp++; while( *cp++ = *src++ ) ; return( dst ); }
以下图所示为一次缓冲区溢出:
与C字符串分歧,SDS 的自动扩容机制完全根绝了发生缓冲区溢出的能够性:当SDS API需要对SDS停止点窜时,API会先检查 SDS 的空间能否满足点窜所需的要求,假如不满足,API会自动将SDS的空间扩大至履行点窜所需的巨细,然后才履行现实的点窜操纵,所以利用 SDS 既不需要手动点窜SDS的空间巨细,也不会出现缓冲区溢出题目他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
SDS 的sds sdscat(sds s, const char *t)方式在字符串拼接时会停止扩容相关操纵他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
sds sdscatsds(sds s, const sds t) {return sdscatlen(s, t, sdslen(t));}sds sdscatlen(sds s, const void *t, size_t len) {// 获得源字符串长度size_t curlen = sdslen(s);// SDS 分派空间(自动扩容机制)s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;// 将方针字符串拷贝至源字符串末端memcpy(s+curlen, t, len);// 更新 SDS 长度sdssetlen(s, curlen+len);// 追加竣事符s[curlen+len] = '0';return s;}
自动扩容机制——sdsMakeRoomFor方式strcatlen中挪用sdsMakeRoomFor完成字符串的容量检查及扩容操纵,重点分析此方式:
sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;// sdsavail: s->alloc - s->len, 获得 SDS 的残剩长度size_t avail = sdsavail(s);size_t len, newlen, reqlen;// 按照 flags 获得 SDS 的范例 oldtypechar type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t usable;// 残剩空间大于即是新增空间,无需扩容,间接返回源字符串if (avail >= addlen) return s;// 获得当前长度len = sdslen(s);//sh = (char*)s-sdsHdrSize(oldtype);// 新长度reqlen = newlen = (len+addlen);// 断言新长度比原长度长,否则停止履行assert(newlen > len); // SDS_MAX_PREALLOC = 1024*1024, 即1MBif (newlen reqlen); if (oldtype==type) {// 范例没变// 挪用 s_realloc_usable 重新分派可用内存,返回新 SDS 的头部指针// usable 会被设备为当前分派的巨细newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);if (newsh == NULL) return NULL; // 分派失利间接返回NULL// 获得指向 buf 的指针s = (char*)newsh+hdrlen;} else {// 范例变化致使 header 的巨细也变化,需要向前移动字符串,不能利用 reallocnewsh = s_malloc_usable(hdrlen+newlen+1, &usable);if (newsh == NULL) return NULL;// 将原字符串copy至新空间中memcpy((char*)newsh+hdrlen, s, len+1);// 开释原字符串内存s_free(sh);s = (char*)newsh+hdrlen;// 更新 SDS 范例s[-1] = type;// 设备长度sdssetlen(s, len);}// 获得 buf 总长度(待定)usable = usable-hdrlen-1;if (usable > sdsTypeMaxSize(type))// 若可用空间大于当前范例支持的最大长度则截断usable = sdsTypeMaxSize(type);// 设备 buf 总长度sdssetalloc(s, usable);return s;}
自动扩容机制总结:
扩容阶段:
若 SDS 中残剩余暇空间 avail 大于新增内容的长度 addlen,则无需扩容;若 SDS 中残剩余暇空间 avail 小于或即是新增内容的长度 addlen: 若新增后总长度 len+addlen 1MB,则按新长度加上 1MB 扩容他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。内存分派阶段:
按照扩容后的长度挑选对应的 SDS 范例: 若范例稳定,则只需经过 s_realloc_usable扩大 buf 数组即可;若范例变化,则需要为全部 SDS 重新分派内存,并将本来的 SDS 内容拷贝至新位置他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。自动扩容流程图以下所示:
扩容后的 SDS 不会恰好包容下新增的字符,而是多分派了一些空间(预分派战略),这削减了点窜字符串时带来的内存重分派次数
4.3.4 内存重分派次数优化(1) 空间预分派战略由于 SDS 的空间预分派战略, SDS 字符串在增加进程中不会频仍的停止空间分派他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。经过这类分派战略,SDS 将持续增加N次字符串所需的内存重分派次数从一定N次下降为最多N次他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
(2) 惰性空间开释机制空间预分派战略用于优化 SDS 增加时频仍停止空间分派,而惰性空间开释机制则用于优化 SDS 字符串收缩时并不立即利用内存重分派往返收收缩后多出来的空间,而仅仅更新 SDS 的len属性,多出来的空间供未来利用他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
SDS 中挪用sdstrim方式来收缩字符串:
sds sdstrim(sds s, const char *cset) {char *start, *end, *sp, *ep;size_t len;sp = start = s;ep = end = s+sdslen(s)-1;// strchr()函数用于查找给定字符串中某一个特定字符while(sp sp && strchr(cset, *ep)) ep--;len = (sp > ep) ? 0 : ((ep-sp)+1);if (s != sp) memmove(s, sp, len);s[len] = '0';// 仅仅更新了lensdssetlen(s,len);return s;}
勘误在《Redis的设想与实现》一书中针对 sdstrim方式的讲授为:删除字符串中 cset 出现的一切字符,而不是首尾他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
比如:挪用sdstrim("XYXaYYbcXyY","XY"),后移除了一切的'X'和'Y'他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。这是毛病❌的~
SDS 并没有开释多出来的5字节空间,仅仅将 len 设备成了7,残剩空间为5他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。假如后续字符串增加时则可以派上用处(能够不需要再分派内存)他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
也许列位看官又会有疑问了,这没真正开释空间,能否会致使内存泄露呢?安心,SDS为我们供给了真正开释SDS未利用空间的方式sdsRemoveFreeSpace他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;// 获得范例char type, oldtype = s[-1] & SDS_TYPE_MASK;// 获得 header 巨细int hdrlen, oldhdrlen = sdsHdrSize(oldtype);// 获得原字符串长度size_t len = sdslen(s);// 获得可用长度size_t avail = sdsavail(s);// 获得指向头部的指针sh = (char*)s-oldhdrlen;if (avail == 0) return s;// 查找合适这个字符串长度的最优 SDS 范例type = sdsReqType(len);hdrlen = sdsHdrSize(type);if (oldtype==type || type > SDS_TYPE_8) {newsh = s_realloc(sh, oldhdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+oldhdrlen;} else {newsh = s_malloc(hdrlen+len+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);}// 重新设备总长度为lensdssetalloc(s, len);return s;}
4.4 SDS 最长几多?Redis 官方给出了最大的字符串容量为 512MB他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。这是为什么呢?
在 Reids3.x 版本中len是利用int修饰的,这就会致使 buf 最长就是2147483647,无形中限制了字符串的最大长度他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
任何细节在源码中都能发现,在_sdsnewlen方式建立 SDS 中城市挪用sdsTypeMaxSize方式获得每品种型所能建立的最大buf长度,不难发现此方式最大的返回值为2147483647,即512MB他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
static inline size_t sdsTypeMaxSize(char type) {if (type == SDS_TYPE_5)return (1
此方式在 Redis3.0.0中是不存在的
4.5 部分 API 源码解读建立SDSRedis 经过sdsnewlen方式建立 SDS他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。在方式中会按照字符串初始化长度挑选合适的范例他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {void *sh;sds s;// 按照初始化长度判定 SDS 的范例char type = sdsReqType(initlen);// SDS_TYPE_5 强迫转换为 SDS_TYPE_8// 这样侧面考证了 sdshdr5 从未被利用,建立这一步就GG了 ੯ੁૂ‧̀͡uif (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;// 获得头部大学int hdrlen = sdsHdrSize(type);// 指向 flags 的指针unsigned char *fp; // 分派的空间size_t usable;// 避免溢出assert(initlen + hdrlen + 1 > initlen); // 分派空间// s_trymalloc_usable: 尝试分派内存,失利则返回NULL// s_malloc_usable: 分派内存大概抛异常[不友爱]sh = trymalloc?s_trymalloc_usable(hdrlen+initlen+1, &usable) :s_malloc_usable(hdrlen+initlen+1, &usable);if (sh == NULL) return NULL;if (init==SDS_NOINIT)init = NULL;else if (!init)memset(sh, 0, hdrlen+initlen+1);// s 此时指向bufs = (char*)sh+hdrlen;// 指向flagsfp = ((unsigned char*)s)-1;usable = usable-hdrlen-1;// 对分歧范例的 SDS 可分派空间停止截断if (usable > sdsTypeMaxSize(type))usable = sdsTypeMaxSize(type);switch(type) {case SDS_TYPE_5: {*fp = type | (initlen len = initlen;sh->alloc = usable;*fp = type;break;}// ... 省略}if (initlen && init)memcpy(s, init, initlen);// 末端增加0s[initlen] = '0';return s;}
经过sdsnewlen方式我们可以获得以下信息:
SDS_TYPE_5 会被强迫转换为 SDS_TYPE_8 范例;建立时默许会在末端加'0';返回值是指向 SDS 结构中 buf 的指针;返回值是char *sds范例,可以兼容部分C函数他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。开释SDS为了优化性能,SDS 供给了不间接开释内存,而是通太重置len到达清空 SDS 目标的方式——sdsclear他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。改方式仅仅将 SDS 的len归零,而buf的空间并为真正被清空,新的数据可以复写,而不用重新申请内存他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
void sdsclear(sds s) {sdssetlen(s, 0);// 设备len为0s[0] = '0';//“清空”buf}
若真正想清空 SDS 则可以挪用sdsfree方式,底层经过挪用s_free开释内存他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
void sdsfree(sds s) {if (s == NULL) return;s_free((char*)s-sdsHdrSize(s[-1]));}
我是敖丙,你晓得的越多,你不晓得的越多,感激列位人材的:点赞、收藏和批评,我们下期见!
文章延续更新,关注后答复【材料】有我预备的一线大厂口试材料和简历模板,有大厂口试完整考点他早就发现系统有个隐藏的缝隙私下花了好几个早晨优化了代码。
|
|