前言
前面一起学习Redis--怎么计算字符串内存空间的占用文章中,介绍了字符串在Redis中的数据结构形态(SDS),以及Redis是怎样组织Key和Value之间的关系,以及对字符串的一些编码规则,来进行内存的优化。共涉及DictEntry、RedisObject、SDS 3种结构,字符串编码、对象共享机制,以及jemalloc 内存分配。Redis一直在寻找时间、空间的一种均衡,这种思想体现在丰富的数据结构模型上。
数据类型以及数据结构的关系
Redis提供了丰富的数据类型,有字符串、List、Set、Sorted Set、Hash,以供开发者在不同的场景下使用。这些数据类型底层在不同的情况下,有不同的数据结构提供支持,具体如下图所示
从图中看到,除了String类型对应一种数据结构动态字符串(SDS),其它的4中类型都有2种数据结构进行支持,其中压缩列表ZipList支持的数据类型最为丰富,今天我们就先围绕着ZipList进行分析他存在的意义,以及他的在服务于上层数据类型时的工作原理。本篇的关键词:字节数组、长度编码
ZipList数据模型
压缩列表本质上,就是一组连续的字节数组,在模型上,将这些连续的数组分为3大部分,分别是header、entry集合、end,其中header由zlbytes+zltail+zllen组成,zlend是一个单字节255(1111 1111),用做ZipList的结尾标识符,如图所示
zlbytes: 压缩列表占用的字节数,用4个字节表示
zltail: 从压缩列表指针,到tail节点指针之间的字节数(位移),用4个字节表示
zllen: entry集合的数量,用2个字节表示
zlend: 压缩列表结束标识符,用1个字节表示
ZIPLIST_ENTRY_HEAD: Entry集合第一个节点的指针
ZIPLIST_ENTRY_TAIL: Entry集合最后一个节点的指针
ZIPLIST_ENTRY_END: ziplist 末端结束标识符的指针
从结构上可以看出,压缩列表最大可以支持2^32字节大小的数据,但entry个数是最大只能记录2^16个,因此如果执行获取元素个数的操作,当压缩列表entry个数超过2^16个,那么就需要遍历entry字节数组了,此时的时间复杂度为O(N)
Entry的结构模型
entry是一个个数据节点,由 prevrawlen、len、value 三部分组成,分别是前置节点占用的字节数、当前节点value占用的字节数、value存储的是数据。
由于zlend 采用255(1111 1111)作为压缩列表的结尾标识,因此每个entry的首字节即prevrawlen首字节是不能出现255数值的,Redis对prevrawlen 进行了编码,254 代表着需要4个字节来标识前置节点的长度;
prevrawlen 字节编码
0-253:一个字节表示即可
>=254:需要占用5个字节,第一个字节值为254,剩余的4个字节来表示前置节点的占用字节数
len标识value需要多少个字节,相比于prevrawlen,len就相对复杂些,len也有字节编码,Redis 通过len 的首字节进行编码encoding,从类型上分为字符型和数值型两种编码。
len 字节编码 encoding
如果value是数值型
encoding=11100000:标识着value是一个需要用64bit表示的数字,即需要8字节
encoding=11010000:标识着value是一个需要用32bit表示的数字,即需要4字节
encoding=11110000:标识着value是一个需要用24bit表示的数字,即需要3字节
encoding=11000000:标识着value是一个需要用16bit表示的数字,即需要2字节
encoding=11111110:标识着value是一个需要用 8bit表示的数字,即需要1字节
当然redis还有一些优化,就是用11110000<encoding< 11111110 来表示0~12的数字
如果value是字符型,encoding< 11000000 ,因为数值型的编码范围在>=11000000范围内
encoding<=00111111:encoding即len值,即当value长度小于64字节时,len用一个字节表示
10000000>encoding>=01000000:encoding高1位为0,高2位为1时,标识len需要两个字节表示, 假设byte1,byte2分别代表两个字节的值;value的长度等(byte1- 64)*256+byte2
encoding=10000000:标识着len占用5个字节,后4个字节来表示value的字节数
图解字节数组的变化
下面通过操作List类型的数据,来看看压缩列表编码机制
执行lpush list 1
Prevrawlen:因为没有前置节点,因此前置节点的字节数是0,由于小于254 ,用一个字节表示
len:由于1是数值型,所以才有数值型编码,且在[0~12]范围内,所以用一个字节表示,且encoding也标识对应的值。11110001 = 0 ,11110010 =1 .....
执行lpush list abcd
value=abcd节点(1+1+4=6byte):
Prevrawlen:因为没有前置节点,因此前置节点的字节数是0,由于小于254 ,用一个字节表示
len:由于abcd是字符串,且value字节数=4 小于 64,因此一个字节表示
value=1 的节点:
Prevrawlen:前置节点存在,且占用6字节,小于254,因此prevrawlen 用一个字节表示
执行lpush list (10abcd...xyz)
value= 10*abcd...xyz(共占用26X10=260字节)
Prevrawlen:因为没有前置节点,因此前置节点的字节数是0,由于小于254 ,用一个字节表示
len:由于value占用260字节,大于63(00111111),小于16383(00111111 11111111),所以占用两个字节,首字节以01000000 即64为起始值,每当第二个节点产生进位,01000000+1,如图首节点65(01000001),第二个节点00000100即4,套用公式,len=(65-64)*256+4 = 260.
value= abcd节点
Prevrawlen:由于前置节点占用的字节数263>254,因此prevrawlen字节数有原来的1个字节调整为5个字节,同样触发级联更新value=1的节点的prevrawlen的值,由原来的6变为10。prevrawlen 后四位字节代表263数值,看着有些奇怪,主要是redis这4个字节进行了大小端转换,转成我们日常理解的就是00000000|00000000|00000001|00000111 这样就是我们比较熟悉的263了。
源码解读
前面我们了解到ZipList结构,以及前后节点之间的关系,编码模型,下面我们通过部分源码+伪代码的形式,了解下ZipList 提供了那些常见的函数
ziplistNew (void)
新建一个空的压缩列表
unsigned char *ziplistNew(void) {
// ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小,即zlbytes+zltail+zllen=4+4+2=10
// 1 字节是表末端 ZIP_END 的大小
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
// 为表头和表末端分配空间,bytes = 11,默认采用jemalloc内存分配器,因此实际申请的内存空间为16byte
unsigned char *zl = zmalloc(bytes);
// 初始化表属性,设置zlbytes 的值=11
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
//设置zltail的值=10
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
//设置zllen的值=0
ZIPLIST_LENGTH(zl) = 0;
// 设置表末端字节的值=255
zl[bytes-1] = ZIP_END;
return zl;
}
ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where)
往压缩列表zl指定位置where添加添加一个长度为slen的字符串,where的其实只是表示在列表头部添加还是尾部添加。需要留意的是,该函数在某些场景下时间复杂度是O(N^2),场景是如果所有entry节点的长度都是250~253时,当头部插入一个大于entry长度大于等于254的节点时,后置节点的prevrawlen都会由原来的1个字节扩展至5个字节,导致后置节点的长度相应的增加4,继而触发后续的节点级联更新。
//T=O(N^2)
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
// 根据 where 参数的值,决定将值推入到表头还是表尾,持有相应位置的指针
unsigned char *p;
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
// 返回添加新值后的 ziplist
// T = O(N^2)
return __ziplistInsert(zl,p,s,slen);
}
/**
* 在zl压缩列表的p的位置,新增一个长度为slen的,value为s的字符串
**/
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// 记录当前 ziplist 的长度
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* initialized to avoid warning. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry entry, tail;
//根据p的位置,计算前置节点的长度,如果在头部,那么prelen=0,如果在队尾且列表不为空,那么prelen=前置节点的长度,否则列表为空,prelen=0;
prelen=getPrelen();
//计算value的len
//此处会尝试进行数值编码转换
reqlen=getValueLen();
/* We need space for both the length of the previous entry and
* the length of the payload. */
// 计算前置节点编码所需的字节大小,即prevrawlen所占用的字节数
// T = O(1)
reqlen += zipPrevEncodeLength(NULL,prevlen);
// 计算编码所需的字节大小,即len所占用的字节数
// T = O(1)
reqlen += zipEncodeLength(NULL,encoding,slen);
//截止目前,新节点的所需字节数已计算完毕,值为reqlen
/* When the insert position is not equal to the tail, we need to
* make sure that the next entry can hold this entry's length in
* its prevlen field. */
// 只要新节点不是被添加到列表末端,
// 那么程序就需要计算新节点的后置节点的prevrawlen占用字节数是否变大,即假如之前后置节点prevrawlen占用 //1个字节,现在新节点占用字节数大于253,那么后置节点prevrawlen所占用的字节数就由1变为5,nextdiff=4
// nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0
// 那么说明需要对 p 所指向的节点(的 header )进行扩展
// T = O(1)
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
/* Store offset because a realloc may change the address of zl. */
// 因为重分配空间可能会改变 zl 的地址
// 所以在分配之前,需要记录 zl 到 p 的偏移量,然后在分配之后依靠偏移量还原 p
offset = p-zl;
// 调整压缩列表的空间,并更新zlbytes的值
// curlen 是 ziplist 原来的长度
// reqlen 是整个新节点的长度
// nextdiff 是新节点的后继节点扩展 header 的长度(要么 0 字节,要么 4 个字节)
// T = O(N)
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
// 新元素之后还有节点,因为新元素的加入,需要对这些原有节点进行调整
/* Subtract one because of the ZIP_END bytes */
// 移动现有元素,为新元素的插入空间腾出位置
// T = O(N)
//memmove(void *dst, const void *src, size_t len)
//q+prelen,即移动到目的地的起始位置
//p-nextdiff,从哪个地方开始拷贝
//curlen-offset-1+nextdiff,需要移动的字节数,即*P后面的字节,除了ZIP_END都需要移动
//注意此处只是新插入节点的后置节点计算了nextdiff,后续的其它节点尚未计算。
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/* Encode this entry's raw length in the next entry. */
// 上面移动完,现在更新下新节点的后置节点的prevrawlen数值。
// p+reqlen 定位到后置节点
// reqlen 是新节点的长度
// T = O(1)
zipPrevEncodeLength(p+reqlen,reqlen);
/* Update offset for tail */
// 更新到达表尾的偏移量,将新节点的长度也算上,这种算法,只适用于新节点后面只有一个后置节点
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
// 如果新节点的后面有多于一个节点
// 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
// 这样才能让表尾偏移量正确对齐表尾节点
// T = O(1)
tail = zipEntry(p+reqlen);//获取新节点的后置节点
//tail.headersize = tail.prevrawlen+编码len的字节数,tail.len=value的字节数
//如果新节点后面不止一个后继节点,那么表尾偏移量在后移nextdiff。
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
/* This element will be the new tail. */
// 新元素是新的表尾节点
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
/* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist */
// 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经被改变,
// 所以需要级联地更新后续的节点
if (nextdiff != 0) {
//级联更新,可能会重新分配zl地址,那么需要先计算出*p的偏移量
offset = p-zl;
// T = O(N^2)
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
/* Write the entry */
// 一切搞定,将前置节点的长度写入新节点的 header
p += zipPrevEncodeLength(p,prevlen);
// 将节点值的长度写入新节点的 header
p += zipEncodeLength(p,encoding,slen);
// 写入节点值
if (ZIP_IS_STR(encoding)) {
// T = O(N)
memcpy(p,s,slen);
} else {
// T = O(1)
zipSaveInteger(p,value,encoding);
}
// 更新列表的节点数量计数器
// T = O(1)
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
从源码中可以看出,新增一个节点,分一下几个步骤
- 确定新节点的插入位置,表头或者表尾
- 数据准备,计算新节点的前置节点长度prelen,新节点的占用字节数 reqlen。
- 计算新节点的第一个后置节点的prevrawlen是否需要申请额外的字节来表示新节点的字节数nextdiff。
- 调整zl压缩列表的长度,更新zl压缩列表的zlbytes值
- 移动复制原有的数据节点,为新节点腾出空间
- 更新新节点的第一个后继节点的prevrawlen的值。
- 更新zl压缩列表的zltail的值
- 如果新节点的第一个后继节点的prevrawlen占用的字节数有调整,则尝试级联更新后继节点的prevrawlen。
- 给新节点设置prevrawlen、len、value的值
- 更新zl压缩列表的zllen的值。
ziplistIndex(unsigned char *zl, int index)
返回指针P,当index 小于0 则从表尾寻址,否则从表头寻址,例如index = -1,那么p指向tail尾结点,如果index=-2 ,那么p指向tail的前置节点。
ziplistNext(unsigned char *zl, unsigned char *p)
返回P指向节点的后置节点,如果p是tail节点,则返回NULL
ziplistPrev(unsigned char *zl, unsigned char *p)
返回P指向节点的前置节点,如果P是头节点,则返回NULL
ziplistGet(unsigned char *p, unsigned char *sval, unsigned int *slen, long long *lval)
获取P指向节点的值,如果是字符串,则将value 赋值给sval,value的长度赋值给slen,如果是数值型,则将value赋值给lval
ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen)
将长度为slen的包含给定值s的新节点,放置到P的位置,如果P指向一个节点,那么新节点放置在原有节点的前面。
ziplistDelete(unsigned char *zl, unsigned char *p)
删除P指向的节点,可能会触发级联更新
ziplistDeleteRange(unsigned char *zl, unsigned int index, unsigned int num)
首先通过ziplistIndex定位P指向的节点,然后连续向后连续删除num个节点。可以联想下list.trim操作
ziplistCompare(unsigned char *p, unsigned char *s, unsigned int slen)
比较P指向的节点值与给定的s值,如果相等,则返回1,否则返回0
ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip)
从P指向的节点开始,以skip为步长,查找与给定的vstr值相同的首个节点,主要应用于hash数据类型的filed查找
ziplistLen(unsigned char *zl)
返回压缩列表的节点个数
ziplistBlobLen(unsigned char *zl)
返回压缩列表占用的内存字节数
几种数据类型在压缩列表中的使用形态
List数据类型
List数据类型的元素无需去重,但需要有序,压缩列表本质的数据模型就是一个个连续的entry数组,因此每个entry节点就代表着一个集合元素,相对理解简单
Sorted Set & Hash数据类型
相比于list,sorted set 包含member 和 score,Hash包含field 和 value,因此可以将member、score、field、value 各个看成entry,比如entry数组中偶数index(包含0) 代表着 member 或者filed,奇数index代表着score或者value。这样查找数据节点时,ziplistFind 通过skip跳过score或者value,然后对比member、filed进行集合意义上的滤重。唯一不同的事,sorted set 是按照score排序的,因此在执行zadd的时候,如果找到对应的member节点后,还要获取score,如果score!=oldScore,直接删除member 和score节点,执行插入操作。而Hash数据类型只需要找到对应的filed节点,更新下value即可。
其它对应的数据结构
双端链表 和 Hash表在Redis中,是怎样的一种结构呢?我们可以通过下图简单了解下,在Redis中的一种组合形态
从图中可以看出,我们可以通过key的计算,算出对应的slot,然后找到对应的dictEntry,dictEntry->val 指向了一个redisObject对象,而redisObject->ptr指向了各类型的数据结构。
List数据结构
/*
* 双端链表结构
*/
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 节点值复制函数,如果不指定相应的复制函数时,所谓的复制是浅复制,即指针指向同一个元素对象
void *(*dup)(void *ptr);
// 节点值释放函数,如果不指定释放函数,则使用zfree释放
void (*free)(void *ptr);
// 节点值对比函数,如果不指定对比函数,就直接进行地址对比,类似于java中字符串的==
int (*match)(void *ptr, void *key);
// 链表所包含的节点数量
unsigned long len;
} list;
ListNode数据结构
/*
* 双端链表节点
*/
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值,此处指针指向的是一个redisObject
void *value;
} listNode;
Hash数据结构
/*
* 字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
dictEntry数据结构
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
压缩列表存在的意义
Redis主要操作对象是内存,对外提供高效的读写,因此涉及到时间、空间两个维度。例如Redis通过dict字典来进行key索引存储,用O(1)时间复杂度来支持key的查询,也会通过rehash来拓展字典的大小,降低hash冲突带来的链式寻址的可能性。从一起学习Redis--怎么计算字符串内存空间的占用文章中其实可以看出,如果我们要想存储一个小的数据时,可能redis自身的数据结构占用的内存空间,会远远的大于数据本身使用的空间。虽然压缩列表的查询操作、插入操作的时间复杂度不是O(1),甚至插入操作会触发O(N^2)的时间复杂度,但是当节点数量zllen较少以及占用字节数zlbyte较小时,通过cpu缓存的特性,性能并不会太低,这也是时间和空间的一种权衡。
从上面List和Hash的数据结构我们可以看出,当要存储数据时,会有额外的指针、属性带来的内存开销,而压缩列表只是单纯的字节数组来进行值存储即可,我们可以通过例子来感知下;假如我们要为list的存储一个字符串,抛开带来的级联更新因素,假如字符串占用x字节,那么需要最少prevrawlen(1)+len(1)+x=2+x,最多需要prevrawlen(5)+len(5)+x = 10+x 字节;然而使用linkedList双端链表,需要prev(8)+next(8)+value=16+value,由于value是redisObject类型,我们从一起学习Redis--怎么计算字符串内存空间的占用这篇博文中知道,redisObject需要(4+4+24)/8+4+8 = 16字节,其中ptr指向了SDS字符串,而SDS需要的空间大小为9+len+free,其中len=x,而listNode需要8+8+8=24->32字节,即总共需要的空间大小为32+16+9+x+free,新建的字符串free=0,需要空间大小需要48+(9+x)[此处根据x大小不同申请的内存空间也不一样,参考jemalloc内存分配机制],如果x非常小,那么额外的数据开销将远远大于ZipList。
下面通过对List数据类型的操作来验证下空间的消耗
127.0.0.1:6379> lpush list a //创建一个压缩列表
(integer) 1
127.0.0.1:6379> object encoding list
"ziplist" //压缩列表
127.0.0.1:6379> info memory
# Memory
used_memory:1022656 //此时的内存占用大小
used_memory_human:998.69K
used_memory_rss:1015808
used_memory_peak:1022656
used_memory_peak_human:998.69K
used_memory_lua:33792
mem_fragmentation_ratio:0.99
mem_allocator:libc
127.0.0.1:6379> lpush list b //添加一个节点,value = b
(integer) 2
127.0.0.1:6379> info memory
# Memory
used_memory:1022672 //此时新增节点b后,新增的内存为1022672-1022656 = 16字节
used_memory_human:998.70K
used_memory_rss:991232
used_memory_peak:1022672
used_memory_peak_human:998.70K
used_memory_lua:33792
mem_fragmentation_ratio:0.97
mem_allocator:libc
127.0.0.1:6379> object encoding list
"ziplist" //表明目前list的编码格式还是压缩列表
---------------------------我是一个可爱的分割线 --------------------------------------------
127.0.0.1:6379> CONFIG set list-max-ziplist-entries 0 //设置不允许使用压缩列表编码
OK
127.0.0.1:6379> del list //删除key
(integer) 1
------------------------------ 继续分割 -------------------------------------------------
127.0.0.1:6379> lpush list a //先通过创建一个list,来初始化字典,主要是减少其它dict等结构内存分配带来的额外计算
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039616
used_memory_human:1015.25K
used_memory_rss:950272
used_memory_peak:1039616
used_memory_peak_human:1015.25K
used_memory_lua:33792
mem_fragmentation_ratio:0.91
mem_allocator:libc
127.0.0.1:6379> lpush list1 a
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039792 //此处供占用1039792-1039616 = 176字节
used_memory_human:1015.42K
used_memory_rss:950272
used_memory_peak:1039792
used_memory_peak_human:1015.42K
used_memory_lua:33792
mem_fragmentation_ratio:0.91
mem_allocator:libc
127.0.0.1:6379> lpush list1 b //再push一个1字节的元素b
(integer) 2
127.0.0.1:6379> info memory
# Memory
used_memory:1039856 //此处额外占用 1039856-1039792=64字节
used_memory_human:1015.48K
used_memory_rss:901120
used_memory_peak:1039856
used_memory_peak_human:1015.48K
used_memory_lua:33792
mem_fragmentation_ratio:0.87
mem_allocator:libc
127.0.0.1:6379>
从上面的测试中可以看出,同样保存一个字符b,不使用压缩列表,是压缩列表的4倍空间占用。
Why 压缩列表新增一个b占用16字节
新增一个值为b的节点,字节占用多了16byte,按理说应该是size(prevrawlen)+size(len)+size(value)=1+1+1= 3 字节才对。
主要是这样的,一个空的ZipList 占用zlbytes+zltail+zllen+zlend= 4+4+2+1=11字节,默认采用jemalloc 内存分配器,分配16字节
lpush list a
此时ZipList使用了11+3=14字节,尚未触发到扩容
lpush list b
此时zipList 使用了14+3=17字节,触发扩容,内存分配器为其分配到了32 byte,因此整体新增16byte
感兴趣的朋友,可以启用压缩列表,然后push a b 后,在push 一个c,发现内存占用是没有变化的。
Why 新建一个list1占用空间为176字节
因为list1创建需要的内存空间需要这几部分组成
size=sizeof(dictEntry)+sizeof(SDS[key])+sizeof(redisObject[v])+sizeof(list)+sizeof(listNode)+sizeof(redisObject[value])+sizeof(SDS[a])
sizeof(dictEntry) = key+v+next = 8+8+8=24->32;
sizeof(SDS[key])=9+len(list1)+free=9+5+0=14->16;
sizeof(redisObject[v]) = 16;
sizeof(list) 8+8+8+8+8+8=48->48;
sizeof(listNode)=8+8+8->32;
sizeof(redisObject[value])=16;
sizeof(SDS[a])=9+len(a)+free=9+1+0=10->16
所以size= 32+16+16+48+32+16+16=176字节
Why 新添加一个元素b占用64字节
size=sizeof(listNode)+sizeof(redisObject)+sizeof(SDS[b])
sizeof(listNode)=8+8+8->32;
sizeof(redisObject[value])=16;
sizeof(SDS[b])=9+len(b)+free=9+1+0=10->16
size = 32+16+16=64字节
同理大家可以计算下Hash类型的空间占用情况,这里就暂时不表了。
最后:一个渣渣程序员,上面是结合一些资料和源码的主观理解,如果有不对或者疑问的地方,欢迎留言探讨,让我们轻松完爆Redis!期待你的加入。