baiyan完整视频:https://segmentfault.com/a/11...今天正式进入redis5源码的学习。Redis是用C语言编写的基于内存的、单进程的、持久化的Key-Value数据库,解决了磁盘访问速度慢的问题,大大提高了数据访问速度,因此常被用作缓存。那么redis为什么那么快呢?让我们先从内部存储数据结构的角度一步步揭开它的神秘面纱。在redis的set、get等常用命令中,尝试使用最多的是string类型。在redis中,存储字符串的数据类型叫做SimpleDynamicString(SDS),在redis中是如何实现的呢?介绍和回顾我们在PHP7源码分析中提到的zend_string结构:struct_zend_string{zend_refcounted_hgc;/*引用计数,与垃圾回收有关,暂不展开*/zend_ulongh;/*多余的hash值,计算数组key避免对hash值重复计算*/size_tlen;/*存储长度*/charval[1];/*灵活的数组,实际存放的是字符串值*/};before[PHP7源码学习]2019-03-13PHPStringNotes文中提到,设计一个存储字符串的结构最重要的是存储它的长度和字符串本身的内容。至于为什么存储长度,是为了解决二进制安全问题,能够访问到常复杂度的字符串长度。详情请参考以上文章。SDS的新旧结构对比在redis3.2.x之前,SDS的存储结构如下:structsdshdr{intlen;//存储长度intfree;//灵活数组剩余空间用于存放字符串内容charbuf[];//灵活数组,真正存储字符串值};以“Redis”字符串为例,看看旧版SDS结构中是如何存储的:free字段为0,表示buf字段没有剩余存储空间,len字段为5。字符串的长度为5。buf字段存储的是真正的字符串内容。“Redis”存储字符串内容的灵活数组。内存大小为6字节,其余字段占用8字节(4+4+6=14wordsSection)在新版本的redis5中,为了进一步减少字符串存储过程中的内存占用,五个特殊的存储结构适合不同字符串长度划分:struct__attribute__((__packed__))sdshdr5{unsignedcharflags;//low三位存储类型,高5位存储字符串的长度,这种字符串存储很少使用charbuf[];//用于存储字符串内容的灵活数组};struct__attribute__((__packed__))sdshdr8{uint8_tlen;//字符串长度uint8_talloc;//分配的总空间unsignedcharflags;//识别哪种存储类型charbuf[];//用于存储字符串内容的灵活数组};struct__attribute__((__packed__))sdshdr16{uint16_tlen;//字符串长度uint16_talloc;//分配的总空间unsignedcharflags;//识别哪种存储类型charbuf[];//用于存储字符串内容的灵活数组};struct__attribute__((__packed__))sdshdr32{uint32_tlen;//字符串长度uint32_talloc;//分配的总空间unsignedcharflags;//识别哪种存储类型charbuf[];//用于存储字符串内容的灵活数组};struct__attribute__((__packed__))sdshdr64{uint64_tlen;//字符串长度uint64_talloc;//总分配空间unsignedcharflags;//判断是哪种存储类型charbuf[];//存储字符串内容的灵活数组};我们可以看到SDS的存储结构从1变成了5,它们的区别在于len字段用于存储字符串的长度,而storage分配了字节的alloc字段的type占用了1、2,4,8字节(不管sdshdr5类型),决定了这个结构最多能存多长时间(2^8/2^16/2^32/2^64)我们注意到这些结构都有__attribute__((__packed__))关键字,它告诉编译器不要执行结构的内存对齐。这个关键字将在下面详细解释。关于什么是结构体内存对齐可以参考【PHP7源码学习】2019-03-08PHP内存管理2笔记。使用gdb查看SDS的存储结构接下来说说之前存储“Redis”的例子。我们需要首先在其上运行gdb以观察“Redis”字符串使用的结构。gdb的步骤如下:先从官网下载源码包,编译启动一个终端,进入redis源码的src目录,在后台启动一个redis-server:./redis-server&然后查看当前redis后台进程的pid:ps-aux|grepredis记录pid,然后用gdb-p命令调试端口(比如端口号是11430):gdb-p11430然后设置断点在setCommand函数处,这个函数用来执行set命令,然后使用c命令执行到断点:(gdb)bsetCommand(gdb)c有了redis服务器,我们需要启动一个redis客户端,然后启动另一个终端(也是在src目录下),启动客户端:./redis-cli然后我们在redis客户端执行set命令,我们设置一个键为Redis,值为1的键值对:127.0.0.1:6379>setRedis1在我们之前的终端返回服务器,我们发现它停在了setCommand:and然后n往下走,直到setGenericCommand函数,s进去,可以看到我们的key“Redis”,是一个rObj结构体(暂且不看),里面的ptr指向的buf字段string结构,强制一下,可以看到字符串内容“Redis”。我们知道这五个结构无论是哪个,第一位一定是标志字段,我们打印它的值,它的值为1。那么1是什么意思呢?用来识别这五个字符串结构中的哪一个:#defineSDS_TYPE_50#defineSDS_TYPE_81#defineSDS_TYPE_162#defineSDS_TYPE_323#defineSDS_TYPE_644其值为1,代表是sdshdr8类型,我们可以画出存储结构图当前字符串的大小:我们可以看到一共占用了3+6=9个字节,比之前的14个字节节省了5个字节。通过细化之前的length和alloc字段(从之前的int改为int8、int16、int32、int64),这样一来redis存储字符串占用的内存空间就会大大节省。内存空间非常宝贵,redis中最常用的数据类型是字符串类型。虽然节省的空间可能看起来很小,但这样做的好处是巨大的,因为它是如此常用。关键字__attribute__((packed))的作用该关键字用于通知编译器不需要结构的内存对齐。为了测试redis字符串结构中__attribute__((packed))关键字的作用,我们编写如下测试代码:#include"stdio.h"intmain(){struct__attribute__((__packed__))sdshdr64{long长线;长长的分配;无符号字符标志;字符缓冲区[];};结构sdshdr64s;s.len=1;s.alloc=2;printf("sizeofsds64是%d",sizeof(s));返回1;我们定义一个结构体,其字段与redis中的string结构体基本相同。如果添加了__attribute__((__packed__)),它不应该是内存对齐的。如果去掉,应该是内存对齐的,会比前一种情况浪费更多的内存,所以对齐会节省内存。我们现在猜测的内存结构图应该是这样的:我们首先验证添加了__attribute__((__packed__)),我们预计它是错位的,gdb中的内存地址如下:我们看到buf确实来自它从地址0x171开始,未对齐。那我们再看另一种情况,去掉__attribute__((__packed__)),然后进行gdb调试:大家看这张图,是不是和上图一模一样(我真的去掉了重新编译!!!)。这说明在当前情况下,灵活数组在redisstring结构体中的起始位置不受是否加__attribute__((__packed__))关键字的影响,紧跟在结构体的后面,所以节省了内存。不成立。(灵活数组不一定在所有情况下都遵循结构,如果把buf的类型改成int就不会遵循,有兴趣的可以自己debug)。那么,这里为什么要加上__attribute__((__packed__)呢?我们换个思路,既然不能节省空间,那能不能节省时间呢?是不是操作非对齐结构会更好更高效,还是写代码更方便和可读性?笔者这里的猜测是在项目中写代码更方便,可读性更好,我的参考如下:sizeof操作符中,返回结构体占用的空间,大小有很大关系和是否对齐,比如上例中的结构体如果没有加__attribute__((__packed__)),则表示需要内存对齐,sizeof(structs)的返回结果应该是24(8+8+8);如果加上__attribute__((__packed__)),说明不需要对齐,返回的结果应该是17(8+8+1),打印一下:结果和我们的预期一致.我们知道在之前使用gdb的时候,rObj的指针直接指向了f的地址灵活数组buf,即字符串内容的起始地址。那怎么知道它的len和alloc的值呢?只需使用bufptr的地址-sizeof(structs)就足够了。这里如果加上__attribute__((__packed__)),返回的结果是17,那么直接做减法,就可以走到结构体的开头,直接读取len的值。如果不加__attribute__((__packed__)),它返回的结果是24,减法会跑错位置。这就是原因。我们在源码中也可以看到,它确实是这样找到当前字符串的结构体头部:#defineSDS_HDR_VAR(T,s)structsdshdr##T*sh=(void*)((s)-(sizeof(structsdshdr##T)));#defineSDS_HDR(T,s)((structsdshdr##T*)((s)-(sizeof(structsdshdr##T))))那么我们可能会问,你不是直接用buf[-1]访问的吗??或者buf[-17],应该也可以访问len。这里笔者简单的猜测可能是之前的写法,在项目的代码实现中更具有可读性和方便性。更深层次的原因还在讨论中。为什么需要alloc字段?在前面的讲解中,我们并没有提到alloc字段的作用。我们知道它是分配给灵活数组用于存储字符串的总字节数。那么记录这个字段有什么作用呢?那就是空间预分配和惰性空间释放的设计思想。空间预分配:当需要扩展SDS的空间时,程序不仅会为SDS的修改分配必要的空间,还会为SDS额外分配未使用的空间。例如,我们将字符串“Redis”扩展为“Redis111”,应用程序不仅分配了3个字节,只是使其正好适合分配的长度,而是分配了一些额外的空间。具体如何分配请看下面的代码注释。下面说说其中一种分配方式,假设它会分配8个字节的内存空间。现在总内存空间为5+8=13,我们只使用了前8个内存空间,剩下5个内存空间没有使用。那我们为什么要这样做呢?这是因为如果我们继续扩容,比如改为“Redis11111”,在扩容SDS空间之前,SDSAPI会先检查未使用的空间是否足够,如果足够,API会直接使用未使用的空间空间。那我们就不用系统调用去申请空间了,只要把额外的“11”放在之前分配的空间里就可以了。这样,使用内存分配的系统调用次数就会大大减少,提高性能和效率。空间预分配代码如下:sdssdsMakeRoomFor(sdss,size_taddlen){void*sh,*newsh;size_tavail=sdsavail(s);//获取当前字符串剩余可用空间size_tlen,newlen;字符类型,oldtype=s[-1]&SDS_TYPE_MASK;诠释hdrlen;/*如果可用空间大于追加部分的长度,说明当前字符串有多余空间,足以容纳扩展后的字符串,不分配额外空间直接返回*/if(avail>=addlen)返回s;len=sdslen(s);sh=(char*)s-sdsHdrSize(oldtype);newlen=(len+addlen);if(newlen
