不同版本的 Redis 的键值对内存占用情况示例

不同版本的 Redis 的键值对内存占用情况示例

文章目录

  • 不同版本的 Redis 的键值对内存占用情况示例
    • Redis 6.0
      • redisObject
      • dictEntry
      • sds
        • 🍀 数据结构
        • 🍀 sdslen() 函数
        • 🍀 sdsReqType() 函数
        • 🍀 sdsHdrSize() 函数
      • 内存分配 - malloc() 函数
        • 🍀 大小类别的计算
        • 🍀 选择合适的 bin
        • 🍀 实际内存块分配
      • set [key] [value]
        • 🍀 sdsdup() 函数
        • 🍀 dictAddRaw() 函数
        • 🍀 dictSetVal() 函数
      • memory usage [key]
        • 🍀 计算 value 的字节数
        • 🍀 计算 key 的字节数
        • 🍀 计算键值对结构体 dictEntry 的字节数
        • 🍀 小结
    • Redis 7.0
      • redisObject
      • dictEntry
      • sds
        • 🍀 数据结构
        • 🍀 sdslen() 函数
        • 🍀 sdsReqType() 函数
        • 🍀 sdsHdrSize() 函数
      • 内存分配 - malloc() 函数
        • 🍀 大小类别的计算
        • 🍀 选择合适的 bin
        • 🍀 实际内存块分配
      • set [key] [value]
        • 🍀 sdsdup() 函数
        • 🍀 dictAddRaw() 函数
        • 🍀 dictSetVal() 函数
      • memory usage [key]
        • 🍀 计算 value 的字节数
        • 🍀 计算 key 的字节数
        • 🍀 计算键值对结构体 dictEntry 的字节数
        • 🍀 计算所在 db 库的字典元数据的字节数
        • 🍀 小结
    • 总结
      • 造成差异的原因
      • memory usage [key] 计算内存使用小结
      • 感悟

本文主要讨论在 Redis 6.0 与 Redis 7.0 中,以下代码设置的键值对的内存使用字节差异:

# 1(6 个 a)
set aaaaaa 12345678# 2
memory usage aaaaaa# 3(7 个 a)
set aaaaaaa 12345678# 4
memory usage aaaaaaa

「1」与「3」两条命令分别设置了键值对,虽然 key 只相差 1 个字符,但在 Redis 6.0 与 Redis 7.0 中使用 memory usage [key] 命令计算出的内存使用字节数有明显差异。

  • Redis 6.0

    127.0.0.1:6379> set aaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaa
    (integer) 48
    127.0.0.1:6379> set aaaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaaa
    (integer) 49
    
  • Redis 7.0

    127.0.0.1:6379> set aaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaa
    (integer) 48
    127.0.0.1:6379> set aaaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaaa
    (integer) 56
    

Redis 6.0

环境:

  • Redis 6.0.8 源码,单机模式环境
  • Ubuntu 24.04.1 LTS,x86_64 架构(64 位操作系统)

redisObject

Redis 中的 value 对象由 redisObject 结构表示。

// 4 + 4 + 24 + 32 + 64 = 128 bits = 16 bytes
typedef struct redisObject {// 4 bitunsigned type:4;// 4 bitunsigned encoding:4;// #define LRU_BITS 24 即 24 bitunsigned lru:LRU_BITS;// 32 bit                      int refcount;// 64 bit(在 64 位操作系统中占 64 bit,在 32 位操作系统中占 32 bit)void *ptr;
} robj;

对象结构里包含的成员变量:

  • type:标识该对象的数据类型,数据类型是指 StringListHashSetZSet 等等。
  • encoding:标识该对象使用的底层数据结构,底层数据结构是指 SDSZipListSkipList 等等。
  • lru:用于内存淘汰策略的最近最少使用或最少频率使用的键值对状态信息。
  • refcount:引用计数。
  • ptr:指向底层数据结构的指针。

struct redisObject 占用字节数为 16,可使用 sizeof(robj) 计算。

dictEntry

Redis 中的键值对由 dictEntry 结构表示。

// 8 + 8 + 8 = 24 bytes
typedef struct dictEntry {// 8 bytesvoid *key;// 8 bytesunion {void *val;uint64_t u64;int64_t s64;double d;} v;// 8 bytesstruct dictEntry *next;
} dictEntry;

对象结构里包含的成员变量:

  • key:存储 key 地址的指针。
  • v:联合体,存储 value 地址或 value 本身的值。
  • next:指向链表中的下一个元素。

struct dictEntry 占用字节数为 24,可根据 sizeof(robj) 计算。

sds

🍀 数据结构
// sds 实际是字符指针的别名,指向的是 sdshdr5、sdshdr8、sdshdr16 等结构体的 buf 字符数组
typedef char *sds;
/* 注意:sdshdr5 不会作为 value 的数据结构 */
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度(不包含结束字符 '\0') */char buf[]; /* 实际存储字符 */
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* 字符串有效长度,不包含结束字符 '\0' */uint8_t alloc; /* 为 buf 字符数组分配了的字节大小,不包含结束字符 '\0' */unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 未使用 */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[];
};

根据内存分配原理,如果我们已知 buf 字符数组的起始地址,那么在此地址的基础上,将地址减去 sizeof(char),得到的地址所存储的内容就是字符变量 flags 的内容。据此,我们就可以得到对应的 sds 类型。 这一点在后面的源码分析中会有体现。

📍 __attribute__ ((__packed__)) 用于告诉编译器进行紧凑字节填充,即忽略默认的对齐规则,不进行任何字节填充。

🍀 sdslen() 函数

作用:返回字符串的有效长度,有效长度并不包含结束字符 '\0'

/* 返回字符串的有效长度 */
static inline size_t sdslen(const sds s) {// sds 实际是 char * 别名,因此 s[-1] 实际上将字符指针存储的地址减去 sizeof(char) 并解引用,得到字符变量 flags 存储的内容 unsigned char flags = s[-1];// 根据 flags 中存储的 sds 类型标识来判断 sds 类型,以正确得到 len 属性值,即字符串有效长度switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: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;
}
🍀 sdsReqType() 函数

作用:根据字符串长度,获取至少应该使用的 sds 数据结构类型的标识。

/* 根据字符串长度 string_size,获取至少应该使用的 sds 数据结构类型的标识。 */
static inline char sdsReqType(size_t string_size) {// 如果字符串长度小于 2^5,则应当使用类型为 sdshr5 的结构体作为 sds 数据结构if (string_size < 1<<5)return SDS_TYPE_5;// 如果字符串长度小于 2^8,则应当使用类型为 sdshr8 的结构体作为 sds 数据结构if (string_size < 1<<8)return SDS_TYPE_8;// 如果字符串长度小于 2^8,则应当使用类型为 sdshr16 的结构体作为 sds 数据结构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
}
🍀 sdsHdrSize() 函数

作用:根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16…)的占用字节大小。

/* 根据类型标识 type,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小。 */
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;
}

内存分配 - malloc() 函数

Redis 选择了使用 jemalloc 作为其默认的内存分配器,因此我们这里关注 jemalloc 对 malloc() 函数的实现。

/* 内存分配,size 是请求分配的内存大小,但实际分配的连续内存块大小 >= size */
void *je_malloc(size_t size) {void *ret;static_opts_t sopts;dynamic_opts_t dopts;LOG("core.malloc.entry", "size: %zu", size);static_opts_init(&sopts);dynamic_opts_init(&dopts);sopts.bump_empty_alloc = true;sopts.null_out_result_on_error = true;sopts.set_errno_on_error = true;sopts.oom_string = "<jemalloc>: Error in malloc(): out of memory\n";dopts.result = &ret;dopts.num_items = 1;/* 将 item_size 设置为请求分配的内存大小 size */dopts.item_size = size;/* imalloc() 函数完成内存分配,并将分配的连续内存块的起始地址存放在 dopts.result 指针指向的地址中,即 ret 中 */imalloc(&sopts, &dopts);LOG("core.malloc.exit", "result: %p", ret);return ret;
}
/* 返回内存分配情况的错误状态码 */
int imalloc(static_opts_t *sopts, dynamic_opts_t *dopts) {// .../* We always need the tsd.  Let's grab it right away. */tsd_t *tsd = tsd_fetch();assert(tsd);if (likely(tsd_fast(tsd))) {/* Fast and common path. */tsd_assert_fast(tsd);sopts->slow = false;/* imalloc_body() 函数完成内存分配,并将分配的连续内存块的起始地址存放在 dopts->result 指针指向的地址中 */ return imalloc_body(sopts, dopts, tsd);} else {sopts->slow = true;return imalloc_body(sopts, dopts, tsd);}
}
int imalloc_body(static_opts_t *sopts, dynamic_opts_t *dopts, tsd_t *tsd) {/* 指向实际分配的内存块的起始地址 */void *allocation = NULL;/* 用于存储请求的内存大小 */size_t size = 0;szind_t ind = 0;size_t usize = 0;/* Reentrancy is only checked on slow path. */int8_t reentrancy_level;/* 计算请求的内存大小,正常情况下,会将 *size = dopts->item_size,也就是将请求的内存大小赋值给 size 变量 */if (unlikely(compute_size_with_overflow(sopts->may_overflow, dopts,&size))) {goto label_oom; // 如果计算过程中发生溢出,则跳转到错误处理标签}// .../* 核心算法开始 */// 如果没有特殊对齐要求,默认情况下 dopts->alignment 为 0if (dopts->alignment == 0) {/* 将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin */ind = sz_size2index(size);// ...} else { // 如果有对齐要求// 根据对齐需求调整大小usize = sz_sa2u(size, dopts->alignment);// ...}// ...// imalloc_no_sample() 函数实际执行内存分配,并返回分配的连续内存块的起始地址allocation = imalloc_no_sample(sopts, dopts, tsd, size, usize, ind);if (unlikely(allocation == NULL)) {goto label_oom;}/* Success! */// 将已分配的内存块的起始地址赋给 *dopts->result*dopts->result = allocation;return 0;// ...
}

整个的内存分配,大致做了三件事情:

  1. 大小类别的计算
  2. 选择合适的 bin
  3. 实际内存块分配
🍀 大小类别的计算

将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin。实际上,该索引不仅可以定位到 tcache_t 中对应的 cache_bin_t 实例,还可以得到请求字节大小对应的实际 jemalloc 应该分配的内存块大小,这个实际分配内存块大小等于 sz_index2size_tab[ind]

szind_t sz_size2index(size_t size) {assert(size > 0);if (likely(size <= LOOKUP_MAXCLASS)) {/* 将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin */return sz_size2index_lookup(size);}return sz_size2index_compute(size);
}
#define LG_TINY_MIN		3szind_t sz_size2index_lookup(size_t size) {assert(size <= LOOKUP_MAXCLASS);{/* * 1.根据 size 计算 sz_size2index_tab 映射表索引:(size-1) >> LG_TINY_MIN* 2.从 sz_size2index_tab 映射表获取定位 bin 的索引:sz_size2index_tab[(size-1) >> LG_TINY_MIN] */szind_t ret = (sz_size2index_tab[(size-1) >> LG_TINY_MIN]);assert(ret == sz_size2index_compute(size));/* 返回用于定位负责处理特定大小内存块的 bin 的索引 */return ret;}
}

这里 jemalloc 实际维护了两张映射表:

  1. sz_size2index_tab:维护了从「请求字节大小」到「索引」的映射。

    /** sz_size2index_tab is a compact lookup table that rounds request sizes up to* size classes.  In order to reduce cache footprint, the table is compressed,* and all accesses are via sz_size2index().*/
    extern uint8_t const sz_size2index_tab[];
    
    数组索引(index)定位 bin/存储的 sz_index2size_tab 数组索引(value)
    00
    11
    22
    33
    44
    55
    66
    77
    88
    98
    109
    119
    1210
  2. sz_index2size_tab:维护了从「索引」到「jemalloc 应该分配的内存块大小」的映射。

    /** sz_index2size_tab encodes the same information as could be computed (at* unacceptable cost in some code paths) by sz_index2size_compute().*/
    extern size_t const sz_index2size_tab[NSIZES];
    
    数组索引(index)jemalloc 应该分配的内存块大小(value)
    08
    116
    224
    332
    440
    548
    656
    764
    880
    996
    10112
    11128
    12160
    13192
    14224
    2336917529027641081856
    2348070450532247928832

📑 例如:

  • 如果请求字节大小 size = 8,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 0,可得 sz_index2size_tab[0] 的值为 8。也就是说,如果请求字节大小为 8,那么 jemalloc 会为其分配 8 字节的连续内存块。
  • 如果请求字节大小 size = 9,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 1,可得 sz_index2size_tab[1] 的值为 16。也就是说,如果请求字节大小为 9,那么 jemalloc 会为其分配 16 字节的连续内存块。
🍀 选择合适的 bin

当应用程序请求分配某个大小的对象时,jemalloc 会计算出最接近且不小于该大小的类别索引,然后使用这个索引来访问 tcache_t 中对应的 cache_bin_t 进行分配。每个 cache_bin_t 实例专用于一个预定义的大小类别,从而实现了对多种不同大小内存块的支持。

也就是在源码中,有大概这样的逻辑:

szind_t ind = sz_size2index(size); // 获取大小类别的索引
cache_bin_t *bin = &tcache->bins_small[ind]; // 获取对应的 bin,以 cache_bin_t	bins_small[39] 数组为例

tcache 是什么呢?

#define NBINS			39
#define NSIZES			235struct tcache_s {// .../** 小对象的缓存 bin 数组* 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i 位置存储的 jemalloc 应该分配的内存块大小相同*/cache_bin_t bins_small[NBINS];/** 大对象的缓存 bin 数组* 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i+NBINS 位置存储的 jemalloc 应该分配的内存块大小相同*/cache_bin_t bins_large[NSIZES - NBINS];
};
🍀 实际内存块分配

首先需要了解 cache_bin_t 结构体:

typedef struct cache_bin_s cache_bin_t;
typedef int32_t cache_bin_sz_t;struct cache_bin_s {cache_bin_sz_t low_water;cache_bin_sz_t ncached;cache_bin_stats_t tstats;void **avail;
};
  • avail:这是一个二级指针,存储了一个指针数组的末端边界地址。指针数组是用于存储一组指向可用内存块的指针。指针数组可看做是一个栈结构,从栈顶 -> 栈底,对应指针数组的首地址 -> 末端边界地址(低地址 -> 高地址),avail 二级指针指向的地址即栈底。
  • ncached:记录当前 bin 中有多少个可用的内存块,每次成功分配时减一,回收时加一。它也是指针数组的元素个数,即可用内存块数量。

avail[-ncached, ..., -1] 是可用内存块的指针,其中最低地址的对象将最先被分配出去。也就是说,当进行内存分配时,将栈顶元素 *(avail - ncached) 弹出,并 ncached--。源码如下:

/* 使用 bin 实例分配对应大小的内存块,返回分配的内存块的首地址 */
void *cache_bin_alloc_easy(cache_bin_t *bin, bool *success) {void *ret;/* 检查 bin 中是否有可用的缓存块 */if (unlikely(bin->ncached == 0)) { // 如果没有可用块bin->low_water = -1; // 设置低水位标记为无效值*success = false; // 分配失败return NULL; // 返回空指针表示分配失败}/* 分配成功 */*success = true;/* * 从 bin 的 avail 栈顶弹出一个内存块地址。* 注意这里的减法操作是因为 avail 指向的是栈底,而 ncached 表示栈中的元素数量。* 这样可以确保我们总是从栈顶获取最新的可用块。*/ret = *(bin->avail - bin->ncached);/* 更新缓存计数器,因为我们刚刚分配了一个块 */bin->ncached--;if (unlikely(bin->ncached < bin->low_water)) {bin->low_water = bin->ncached;}/* 返回分配的内存块地址 */return ret;
}

set [key] [value]

set [key] [value] 命令对应的处理函数为 setCommand

以在命令执行前,db 中不存在该 key 为例,setCommand() 函数会调用到核心处理函数 dbAdd()

/* 将 key-value 添加到 db 中 */
void dbAdd(redisDb *db, robj *key, robj *val) {// sds 是 char* 的别名,通过 sdsdup() 函数得到的实际是表示 key 的 sds 结构体 buf 字符数组首元素地址sds copy = sdsdup(key->ptr);int retval = dictAdd(db->dict, copy, val);serverAssertWithInfo(NULL,key,retval == DICT_OK);if (val->type == OBJ_LIST ||val->type == OBJ_ZSET ||val->type == OBJ_STREAM)signalKeyAsReady(db, key);if (server.cluster_enabled) slotToKeyAdd(key->ptr);
}int dictAdd(dict *d, void *key, void *val)
{// 1.在堆中开辟 dictEntry 结构体对象空间// 2.将 dictEntry 存储在 db 字典中// 3.将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中// 4.返回 dictEntry 结构体指针dictEntry *entry = dictAddRaw(d,key,NULL);if (!entry) return DICT_ERR;// 将 value 设置到 dictEntry 结构体对象 dictSetVal(d, entry, val);return DICT_OK;
}

我们重点关注源码中以下三个函数的作用:

  • sdsdup(key->ptr):拷贝 sds,并返回字符数组的首元素地址。
  • dictAddRaw(d, key, NULL)
    1. 在堆中开辟 dictEntry 结构体对象空间;
    2. 将 dictEntry 存储在 db 字典中;
    3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
    4. 返回 dictEntry 结构体指针。
  • dictSetVal(d, entry, val):将 value 设置到 dictEntry 结构体对象
🍀 sdsdup() 函数

作用:拷贝 sds,并返回字符数组的首元素地址。

sds sdsdup(const sds s) {/* sdslen(s) 返回字符串的有效长度 */return sdsnewlen(s, sdslen(s));
}
sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;/* 根据初始化长度获取至少应该使用的 sds 数据结构类型的标识 */char type = sdsReqType(initlen);/* 空字符串通常为拼接而创建的,因此使用 sdshdr8 作为 sds 数据结构比 sdshdr5 更加合适 */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;/* 根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小 *//* 由于实际存储字符串的 char buf[] 是结构体最后一个成员,因此这是一个柔性数组,占用字节不会计算在使用 sizeof() 得到的结构体占用字节范围内 */int hdrlen = sdsHdrSize(type);/* 指向 sds 类型 —— flags 变量的指针 */unsigned char *fp;/* 为 sds 数据结构分配堆内存 *//* 请求字节大小为 hdrlen+initlen+1 = sds 结构体大小 + 字符串有效长度 + 结束字符 '\0' 的 1 个字节 */sh = s_malloc(hdrlen+initlen+1);// .../* 将预期字符数组 buf 的起始地址存储到 char* s 中 */s = (char*)sh+hdrlen;/* 将 flags 变量的地址存储到 unsigned char *fp 中 */fp = ((unsigned char*)s)-1;/* 根据类型标识,对 sds 类型的实现数据类型结构体进行属性配置 */switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS); /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度 */break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s); /* SDS_HDR_VAR 是一个宏函数,作用是将 sh 指针指向 sds 结构体的起始地址,以操作结构体 */sh->len = initlen; /* 设置字符串有效长度 */sh->alloc = initlen; /* 设置为 buf 字符数组分配了的字节大小 */*fp = type; /* 设置 sds 类型 */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);/* 为了兼容 c 标准字符串函数,以 '\0' 作为字符数组结束标识 */s[initlen] = '\0';/* 返回字符数组 buf 的起始地址 */return s;
}

对应的:

  • key = “aaaaaa”,字节数 initlen 为 6,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc() 函数时请求字节大小为 hdrlen+initlen+1 = 1+6+1 = 8,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 0,而 sz_index2size_tab[0] 的值为 8,即实际分配内存块大小为 8。在配置 sdshdr5 实例属性时,设置 alloc = initlen = 6

    image-20241216185748945

  • key = “aaaaaaa”,字节数 initlen 为 7,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc_usable() 函数时请求字节大小为 hdrlen+initlen+1 = 1+7+1 = 9,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[0] 的值为 16,即实际分配内存块大小为 16。在配置 sdshdr5 实例属性时,设置 alloc = initlen = 7。也就是说,Redis 不会将多分配的 7 字节作为字符数组 buf 的空间使用。

    image-20241217172017841

🍀 dictAddRaw() 函数

作用:

  1. 在堆中开辟 dictEntry 结构体对象空间;
  2. 将 dictEntry 存储在 db 字典中;
  3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
  4. 返回 dictEntry 结构体指针。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;dictht *ht;if (dictIsRehashing(d)) _dictRehashStep(d);/* 计算 key 在 dict 哈希字典中的索引 */if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)return NULL;/* 由于可能的扩容,因此存在两个 dict,需要判断使用使用哪一个 */ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];/* 在堆中开辟 dictEntry 结构体对象空间 */entry = zmalloc(sizeof(*entry));/* 采用头插法,并以链表形式,将 dictEntry 存储到字典索引位置 */entry->next = ht->table[index];ht->table[index] = entry;/* key 计数 +1 */ht->used++;/* dictSetKey 是一个宏,会替换为 entry->key = key 即将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中 */dictSetKey(d, entry, key);/* 返回 dictEntry 结构体指针 */return entry;
}

对应的:

  • key = “aaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241216192312279

  • key = “aaaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241217172727903

🍀 dictSetVal() 函数

作用:将 value 设置到 dictEntry 结构体对象。这实际是一个宏函数,在预编译时期完成替换。

#define dictSetVal(d, entry, _val_) do { \if ((d)->type->valDup) \(entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \else \(entry)->v.val = (_val_); \
} while(0)

这一步很简单,就是设置 dictEntry -> v.val 指针指向。但我们要重点关注的是 dictEntry -> v.val 指针或者说 robj *val 指针指向的结构体信息,因为这个结构体是 value 的实际内存存储与占用内容。

这里只看 value = “12345678” 的源码部分。由于 “12345678” 可用整型表示,为了节约内存,Redis 会使用 OBJ_ENCODING_INT 编码来进行优化。

// 返回 value 对应的 redisObject 结构体的指针
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {robj *o;// ...if (value >= LONG_MIN && value <= LONG_MAX) {// 创建一个 type = OBJ_STRING 的 redisObject 结构体,sizeof(struct redisObject) 为 16 字节o = createObject(OBJ_STRING, NULL);// 设置编码为 OBJ_ENCODING_INTo->encoding = OBJ_ENCODING_INT;// 复用指针变量,节省内存,把 12345678 当做地址存储。在 get 时,会根据 encoding 再从 ptr 取出值o->ptr = (void*)((long)value);}// ...return o;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241216195602245

  • key = “aaaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241217173306502

memory usage [key]

memory usage [key] 命令对应的处理函数为 memoryCommand

void memoryCommand(client *c) {// ...// 1.计算 value 的字节数size_t usage = objectComputeSize(dictGetVal(de),samples);// 2.计算 key 的字节数usage += sdsAllocSize(dictGetKey(de));// 3.计算键值对结构体 dictEntry 的字节数usage += sizeof(dictEntry);// ...
}

usage 变量即是用于存储键值对的内存使用字节数。可以看到,共有三个部分组成:

  1. objectComputeSize(dictGetVal(de),samples):计算 value 的字节数。
  2. sdsAllocSize(dictGetKey(de)):计算 key 的字节数。
  3. sizeof(dictEntry):计算键值对结构体 dictEntry 的字节数。
🍀 计算 value 的字节数
size_t objectComputeSize(robj *o, size_t sample_size) {sds ele, ele2;dict *d;dictIterator *di;struct dictEntry *de;size_t asize = 0, elesize = 0, samples = 0;if (o->type == OBJ_STRING) {if(o->encoding == OBJ_ENCODING_INT) { // 执行第 1 个 if 中的语句asize = sizeof(*o); // sizeof(struct redisObject) = 16 bytes} else if(o->encoding == OBJ_ENCODING_RAW) {asize = sdsAllocSize(o->ptr)+sizeof(*o);} else if(o->encoding == OBJ_ENCODING_EMBSTR) {asize = sdslen(o->ptr)+2+sizeof(*o);} else {serverPanic("Unknown string encoding");}} else if (o->type == OBJ_LIST) {// ...} else if (o->type == OBJ_SET) {// ...} else if (o->type == OBJ_ZSET) {// ...} else if (o->type == OBJ_HASH) {// ...} else if (o->type == OBJ_STREAM) {// ...} else if (o->type == OBJ_MODULE) {// ...} else {serverPanic("Unknown object type");}return asize;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
  • key = “aaaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
🍀 计算 key 的字节数
size_t sdsAllocSize(sds s) {// 获取 sds 结构体的 alloc 属性值,这实际是为字符数组 buf 开辟了的内存大小(不包含结束字符 '\0')size_t alloc = sdsalloc(s);// sds 结构体占用字节 + 为字符数组 buf 开辟了的内存大小 + 结束字符 '\0' 1 个字节// 这实际是之前 set 时对 key 进行内存分配计算出的请求内存大小,而非实际分配内存大小,redis 6.0 没有使用这多分配的空间return sdsHdrSize(s[-1])+alloc+1;
}/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->alloc;case SDS_TYPE_16:return SDS_HDR(16,s)->alloc;case SDS_TYPE_32:return SDS_HDR(32,s)->alloc;case SDS_TYPE_64:return SDS_HDR(64,s)->alloc;}return 0;
}

对应的:

  • key = “aaaaaa”,通过之前对 sdsdup() 函数的分析,请求内存大小为 sizeof(struct sdshdr5)+alloc+1=1+6+1=8
  • key = “aaaaaaa”,通过之前对 sdsdup() 函数的分析,请求内存大小为 sizeof(struct sdshdr5)+alloc+1=1+7+1=9
🍀 计算键值对结构体 dictEntry 的字节数
typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;
} dictEntry;

占用字节分析:

  • *void key:8 字节。
  • union v:联合体,8 字节。
  • *struct dictEntry next:8 字节。

综上,sizeof(struct dictEntry) 的结果为 24 字节。

🍀 小结

综上对每个函数的分析,以及 set 时的具体实现,我们得出:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数89
计算键值对结构体 dictEntry 的字节数2424
字节总和4849

Redis 7.0

  • Redis 7.0.14 源码,单机模式环境
  • Ubuntu 24.04.1 LTS,x86_64 架构(64 位操作系统)

redisObject

Redis 中的 value 对象由 redisObject 结构表示。

// 4 + 4 + 24 + 32 + 64 = 128 bits = 16 bytes
typedef struct redisObject {// 4 bitunsigned type:4;// 4 bitunsigned encoding:4;// #define LRU_BITS 24 即 24 bitunsigned lru:LRU_BITS;// 32 bit                      int refcount;// 64 bit(在 64 位操作系统中占 64 bit,在 32 位操作系统中占 32 bit)void *ptr;
} robj;

对象结构里包含的成员变量:

  • type:标识该对象的数据类型,数据类型是指 StringListHashSetZSet 等等。
  • encoding:标识该对象使用的底层数据结构,底层数据结构是指 SDSZipListSkipList 等等。
  • lru:用于内存淘汰策略的最近最少使用或最少频率使用的键值对状态信息。
  • refcount:引用计数。
  • ptr:指向底层数据结构的指针。

struct redisObject 占用字节数为 16,可使用 sizeof(robj) 计算。

dictEntry

Redis 中的键值对由 dictEntry 结构表示。

// 8 + 8 + 8 = 24 bytes
typedef struct dictEntry {// 8 bytesvoid *key;// 8 bytesunion {void *val;uint64_t u64;int64_t s64;double d;} v;// 8 bytesstruct dictEntry *next;// 空指针数组,由于是结构体最后一个成员,因此是柔性数组,不参与结构体占用字节大小计算void *metadata[];
} dictEntry;

对象结构里包含的成员变量:

  • key:存储 key 地址的指针。
  • v:联合体,存储 value 地址或 value 本身的值。
  • next:指向链表中的下一个元素。
  • metadata:存储与键值对相关的额外信息。

struct dictEntry 占用字节数为 24,可根据 sizeof(robj) 计算。

sds

🍀 数据结构
// sds 实际是字符指针的别名,指向的是 sdshdr5、sdshdr8、sdshdr16 等结构体的 buf 字符数组
typedef char *sds;
/* 注意:sdshdr5 不会作为 value 的数据结构 */
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度(不包含结束字符 '\0') */char buf[]; /* 实际存储字符 */
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* 字符串有效长度,不包含结束字符 '\0' */uint8_t alloc; /* 为 buf 字符数组分配了的字节大小,不包含结束字符 '\0' */unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 未使用 */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[];
};

根据内存分配原理,如果我们已知 buf 字符数组的起始地址,那么在此地址的基础上,将地址减去 sizeof(char),得到的地址所存储的内容就是字符变量 flags 的内容。据此,我们就可以得到对应的 sds 类型。 这一点在后面的源码分析中会有体现。

📍 __attribute__ ((__packed__)) 用于告诉编译器进行紧凑字节填充,即忽略默认的对齐规则,不进行任何字节填充。

🍀 sdslen() 函数

作用:返回字符串的有效长度,有效长度并不包含结束字符 '\0'

/* 返回字符串的有效长度 */
static inline size_t sdslen(const sds s) {// sds 实际是 char * 别名,因此 s[-1] 实际上将字符指针存储的地址减去 sizeof(char) 并解引用,得到字符变量 flags 存储的内容 unsigned char flags = s[-1];// 根据 flags 中存储的 sds 类型标识来判断 sds 类型,以正确得到 len 属性值,即字符串有效长度switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: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;
}
🍀 sdsReqType() 函数

作用:根据字符串长度,获取至少应该使用的 sds 数据结构类型的标识。

/* 根据字符串长度 string_size,获取至少应该使用的 sds 数据结构类型的标识。 */
static inline char sdsReqType(size_t string_size) {// 如果字符串长度小于 2^5,则应当使用类型为 sdshr5 的结构体作为 sds 数据结构if (string_size < 1<<5)return SDS_TYPE_5;// 如果字符串长度小于 2^8,则应当使用类型为 sdshr8 的结构体作为 sds 数据结构if (string_size < 1<<8)return SDS_TYPE_8;// 如果字符串长度小于 2^8,则应当使用类型为 sdshr16 的结构体作为 sds 数据结构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
}
🍀 sdsHdrSize() 函数

作用:根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16…)的占用字节大小。

/* 根据类型标识 type,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小。 */
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;
}

内存分配 - malloc() 函数

Redis 选择了使用 jemalloc 作为其默认的内存分配器,因此我们这里关注 jemalloc 对 malloc() 函数的实现。

整个的内存分配,大致做了三件事情:

  1. 大小类别的计算
  2. 选择合适的 bin
  3. 实际内存块分配
void *je_malloc(size_t size) {// .../* * 1.大小类别的计算* 将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin */szind_t ind = sz_size2index_lookup(size);// .../* * 2.选择合适的 bin* 从 tcache 中获取对应大小类别的缓存 bin */cache_bin_t *bin = tcache_small_bin_get(tcache, ind);bool tcache_success;/* * 3.实际内存块分配* 尝试从 bin 中分配内存,如果成功则设置 tcache_success 为 true,并返回分配的连续内存的起始地址 */void* ret = cache_bin_alloc_easy(bin, &tcache_success);/* 如果分配成功 */if (tcache_success) {// .../* 返回分配的连续内存的起始地址 */return ret;}/* 如果上述过程未能成功分配内存,则使用默认的内存分配方法 */return malloc_default(size);
}
🍀 大小类别的计算

将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin。实际上,该索引不仅可以定位到 tcache_t 中对应的 cache_bin_t 实例,还可以得到请求字节大小对应的实际 jemalloc 应该分配的内存块大小,这个实际分配内存块大小等于 sz_index2size_tab[ind]

#define SC_LG_TINY_MIN 3szind_t sz_size2index_lookup(size_t size) {assert(size <= SC_LOOKUP_MAXCLASS);/* * 1.根据 size 计算 sz_size2index_tab 映射表索引:(size + (ZU(1) << SC_LG_TINY_MIN) - 1) >> SC_LG_TINY_MIN* 2.从 sz_size2index_tab 映射表获取定位 bin 的索引:sz_size2index_tab[(size + (ZU(1) << SC_LG_TINY_MIN) - 1) >> SC_LG_TINY_MIN] */szind_t ret = (sz_size2index_tab[(size + (ZU(1) << SC_LG_TINY_MIN) - 1)>> SC_LG_TINY_MIN]);assert(ret == sz_size2index_compute(size));/* 返回存储的 sz_index2size_tab 数组索引 */return ret;
}

这里 jemalloc 实际维护了两张映射表:

  1. sz_size2index_tab:维护了从「请求字节大小」到「索引」的映射。

    uint8_t sz_size2index_tab[(SC_LOOKUP_MAXCLASS >> SC_LG_TINY_MIN) + 1];/* 以下初始化映射表代码不必做了解 */
    static void sz_boot_size2index_tab(const sc_data_t *sc_data) {size_t dst_max = (SC_LOOKUP_MAXCLASS >> SC_LG_TINY_MIN) + 1;size_t dst_ind = 0;for (unsigned sc_ind = 0; sc_ind < SC_NSIZES && dst_ind < dst_max;sc_ind++) {const sc_t *sc = &sc_data->sc[sc_ind];size_t sz = (ZU(1) << sc->lg_base)+ (ZU(sc->ndelta) << sc->lg_delta);size_t max_ind = ((sz + (ZU(1) << SC_LG_TINY_MIN) - 1)>> SC_LG_TINY_MIN);for (; dst_ind <= max_ind && dst_ind < dst_max; dst_ind++) {sz_size2index_tab[dst_ind] = sc_ind;}}
    }
    
    数组索引(index)存储的 sz_index2size_tab 数组索引(value)
    00
    10
    21
    32
    43
    54
    65
    76
    87
    98
  2. sz_index2size_tab:维护了从「索引」到「jemalloc 应该分配的内存块大小」的映射。

    size_t sz_index2size_tab[SC_NSIZES];/* 以下初始化映射表代码不必做了解 */
    static void sz_boot_index2size_tab(const sc_data_t *sc_data) {for (unsigned i = 0; i < SC_NSIZES; i++) {const sc_t *sc = &sc_data->sc[i];sz_index2size_tab[i] = (ZU(1) << sc->lg_base)+ (ZU(sc->ndelta) << (sc->lg_delta));}
    }
    
    数组索引(index)存储的内存块大小(value)
    08
    116
    224
    332
    440
    548
    656
    764
    880
    996
    10112
    11128
    12160
    13192
    14224
    2336917529027641081856
    2348070450532247928832

📑 例如:

  • 如果请求字节大小 size = 8,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 0,可得 sz_index2size_tab[0] 的值为 8。也就是说,如果请求字节大小为 8,那么 jemalloc 会为其分配 8 字节的连续内存块。
  • 如果请求字节大小 size = 9,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 1,可得 sz_index2size_tab[1] 的值为 16。也就是说,如果请求字节大小为 9,那么 jemalloc 会为其分配 16 字节的连续内存块。
🍀 选择合适的 bin

当应用程序请求分配某个大小的对象时,jemalloc 会计算出最接近且不小于该大小的类别索引,然后使用这个索引来访问 tcache_t 中对应的 cache_bin_t 进行分配。每个 cache_bin_t 实例专用于一个预定义的大小类别,从而实现了对多种不同大小内存块的支持。

也就是在源码中,有大概这样的逻辑:

szind_t ind = sz_size2index(size); // 获取大小类别的索引
cache_bin_t *bin = &tcache->bins_small[ind]; // 获取对应的 bin,以 cache_bin_t	bins_small[39] 数组为例

tcache 是什么呢?

typedef struct tcache_s tcache_t;struct tcache_s {// .../** 小对象的缓存 bin 数组* 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i 位置存储的 jemalloc 应该分配的内存块大小相同*/cache_bin_t bins_small[SC_NBINS];/** 大对象的缓存 bin 数组* 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i+SC_NBINS 位置存储的 jemalloc 应该分配的内存块大小相同*/cache_bin_t bins_large[SC_NSIZES-SC_NBINS];
};
🍀 实际内存块分配

首先需要了解 cache_bin_t 结构体:

typedef struct cache_bin_s cache_bin_t;
typedef int32_t cache_bin_sz_t;struct cache_bin_s {cache_bin_sz_t low_water;cache_bin_sz_t ncached;cache_bin_stats_t tstats;void **avail;
};
  • avail:这是一个二级指针,存储了一个指针数组的末端边界地址。指针数组是用于存储一组指向可用内存块的指针。指针数组可看做是一个栈结构,从栈顶 -> 栈底,对应指针数组的首地址 -> 末端边界地址(低地址 -> 高地址),avail 二级指针指向的地址即栈底。
  • ncached:记录当前 bin 中有多少个可用的内存块,每次成功分配时减一,回收时加一。它也是指针数组的元素个数,即可用内存块数量。

avail[-ncached, ..., -1] 是可用内存块的指针,其中最低地址的对象将最先被分配出去。也就是说,当进行内存分配时,ncached--,并将栈顶元素 *(avail - (ncached + 1)) 弹出。源码如下:

/* 使用 bin 实例分配对应大小的内存块,返回分配的内存块的首地址 */
void *cache_bin_alloc_easy(cache_bin_t *bin, bool *success) {void *ret;/* 更新缓存计数器,因为我们准备分配一个块 */bin->ncached--;/* 检查 bin 中是否有可用的缓存块 */if (unlikely(bin->ncached <= bin->low_water)) {bin->low_water = bin->ncached;if (bin->ncached == -1) {bin->ncached = 0;*success = false;return NULL;}}/* 分配成功 */*success = true;/* * 从 bin 的 avail 栈顶弹出一个内存块地址。* 注意这里的减法操作是因为 avail 指向的是栈底,而 ncached 表示栈中的元素数量。* 这样可以确保我们总是从栈顶获取最新的可用块。*/ret = *(bin->avail - (bin->ncached + 1));/* 返回分配的内存块地址 */return ret;
}

set [key] [value]

set [key] [value] 命令对应的处理函数为 setCommand

以在命令执行前,db 中不存在该 key 为例,setCommand() 函数会调用到核心处理函数 dbAdd()

/* 将 key-value 添加到 db 中 */
void dbAdd(redisDb *db, robj *key, robj *val) {// sds 是 char* 的别名,通过 sdsdup() 函数得到的实际是表示 key 的 sds 结构体 buf 字符数组首元素地址sds copy = sdsdup(key->ptr);// 1.在堆中开辟 dictEntry 结构体对象空间// 2.将 dictEntry 存储在 db 字典中// 3.将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中// 4.返回 dictEntry 结构体指针dictEntry *de = dictAddRaw(db->dict, copy, NULL);serverAssertWithInfo(NULL, key, de != NULL);// 将 value 设置到 dictEntry 结构体对象 dictSetVal(db->dict, de, val);signalKeyAsReady(db, key, val->type);if (server.cluster_enabled) slotToKeyAddEntry(de, db);notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
}

我们重点关注源码中以下三个函数的作用:

  • sdsdup(key->ptr):拷贝 sds,并返回字符数组的首元素地址。
  • dictAddRaw(db->dict, copy, NULL)
    1. 在堆中开辟 dictEntry 结构体对象空间;
    2. 将 dictEntry 存储在 db 字典中;
    3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
    4. 返回 dictEntry 结构体指针。
  • dictSetVal(db->dict, de, val):将 value 设置到 dictEntry 结构体对象
🍀 sdsdup() 函数

作用:拷贝 sds,并返回字符数组的首元素地址。

sds sdsdup(const sds s) {/* sdslen(s) 返回字符串的有效长度 */return sdsnewlen(s, sdslen(s));
}
sds sdsnewlen(const void *init, size_t initlen) {return _sdsnewlen(init, initlen, 0);
}
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {void *sh;sds s;/* 根据初始化长度获取至少应该使用的 sds 数据结构类型的标识 */char type = sdsReqType(initlen);/* 空字符串通常为拼接而创建的,因此使用 sdshdr8 作为 sds 数据结构比 sdshdr5 更加合适 */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;/* 根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小 *//* 由于实际存储字符串的 char buf[] 是结构体最后一个成员,因此这是一个柔性数组,占用字节不会计算在使用 sizeof() 得到的结构体占用字节范围内 */int hdrlen = sdsHdrSize(type);/* 指向 sds 类型 —— flags 变量的指针 */unsigned char *fp;size_t usable;/* 检查 size_t 溢出 */assert(initlen + hdrlen + 1 > initlen);/* 为 sds 数据结构分配堆内存,并将 jemalloc 实际分配的字节大小记录在 usable 中 *//* 请求字节大小为 hdrlen+initlen+1 = sds 结构体大小 + 字符串有效长度 + 结束字符 '\0' 的 1 个字节 */sh = trymalloc?s_trymalloc_usable(hdrlen+initlen+1, &usable) :s_malloc_usable(hdrlen+initlen+1, &usable);// .../* 将预期字符数组的起始地址存储到 char* s 中 */s = (char*)sh+hdrlen;/* 将 flags 变量的地址存储到 unsigned char *fp 中 */fp = ((unsigned char*)s)-1;/* 获取柔性数组 char buf[] 可用字节大小 *//* usable = 总共分配的堆内存字节大小 - sizeof(sds 结构体) - 结束标识 '\0' 占 1 个字节 */ usable = usable-hdrlen-1;if (usable > sdsTypeMaxSize(type))usable = sdsTypeMaxSize(type);/* 根据类型标识,对 sds 类型的实现数据类型结构体进行属性配置 */switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS); /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度 */break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s); /* SDS_HDR_VAR 是一个宏函数,作用是将 sh 指针指向 sds 结构体的起始地址,以操作结构体 */sh->len = initlen; /* 设置字符串有效长度 */sh->alloc = usable; /* 设置为 buf 字符数组分配了的字节大小 */*fp = type; /* 设置 sds 类型 */break;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);sh->len = initlen;sh->alloc = usable;*fp = type;break;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);sh->len = initlen;sh->alloc = usable;*fp = type;break;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);sh->len = initlen;sh->alloc = usable;*fp = type;break;}}/* 字符数组拷贝 */if (initlen && init)memcpy(s, init, initlen);/* 为了兼容 c 标准字符串函数,以 '\0' 作为字符数组结束标识 */s[initlen] = '\0';/* 返回字符数组 buf 的起始地址 */return s;
}

对应的:

  • key = “aaaaaa”,字节数 initlen 为 6,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc_usable() 函数时请求字节大小为 hdrlen+initlen+1 = 1+6+1 = 8,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 0,而 sz_index2size_tab[0] 的值为 8,即实际分配内存块大小 *usable = 8。在配置 sdshdr5 实例属性时,设置 alloc = usable - hdrlen - 1 = 6

    image-20241216185748945

  • key = “aaaaaaa”,字节数 initlen 为 7,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc_usable() 函数时请求字节大小为 hdrlen+initlen+1 = 1+7+1 = 9,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[0] 的值为 16,即实际分配内存块大小 *usable = 16。在配置 sdshdr5 实例属性时,设置 alloc = usable - hdrlen - 1 = 14。也就是说,Redis 会将多分配的 7 字节作为字符数组 buf 的空间使用。

    image-20241216185817078

🍀 dictAddRaw() 函数

作用:

  1. 在堆中开辟 dictEntry 结构体对象空间;
  2. 将 dictEntry 存储在 db 字典中;
  3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
  4. 返回 dictEntry 结构体指针。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;int htidx;if (dictIsRehashing(d)) _dictRehashStep(d);/* 计算 key 在 dict 哈希字典中的索引 */if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)return NULL;/* 由于可能的扩容,因此存在两个 dict,需要判断使用使用哪一个 */htidx = dictIsRehashing(d) ? 1 : 0;/* 字典元数据大小,在单机模式下,默认为 0 */size_t metasize = dictMetadataSize(d);/* 在堆中开辟 dictEntry 结构体对象空间 */entry = zmalloc(sizeof(*entry) + metasize);/* 如果有字典元数据,则将 (&entry)->metadata 的 metasize 个字节初始化为 0 */if (metasize > 0) {memset(dictMetadata(entry), 0, metasize);}/* 采用头插法,并以链表形式,将 dictEntry 存储到字典索引位置 */entry->next = d->ht_table[htidx][index];d->ht_table[htidx][index] = entry;/* key 计数 +1 */d->ht_used[htidx]++;/* dictSetKey 是一个宏,会替换为 entry->key = key 即将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中 */dictSetKey(d, entry, key);/* 返回 dictEntry 结构体指针 */return entry;
}

对应的:

  • key = “aaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241216192312279

  • key = “aaaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241216192454026

🍀 dictSetVal() 函数

作用:将 value 设置到 dictEntry 结构体对象。这实际是一个宏函数,在预编译时期完成替换。

#define dictSetVal(d, entry, _val_) do { \if ((d)->type->valDup) \(entry)->v.val = (d)->type->valDup((d), _val_); \else \(entry)->v.val = (_val_); \
} while(0)

这一步很简单,就是设置 dictEntry -> v.val 指针指向。但我们要重点关注的是 dictEntry -> v.val 指针或者说 robj *val 指针指向的结构体信息,因为这个结构体是 value 的实际内存存储与占用内容。

这里只看 value = “12345678” 的源码部分。由于 “12345678” 可用整型表示,为了节约内存,Redis 会使用 OBJ_ENCODING_INT 编码来进行优化。

// 返回 value 对应的 redisObject 结构体的指针
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {robj *o;// ...if (value >= LONG_MIN && value <= LONG_MAX) {// 创建一个 type = OBJ_STRING 的 redisObject 结构体,sizeof(struct redisObject) 为 16 字节o = createObject(OBJ_STRING, NULL);// 设置编码为 OBJ_ENCODING_INTo->encoding = OBJ_ENCODING_INT;// 复用指针变量,节省内存,把 12345678 当做地址存储。在 get 时,会根据 encoding 再从 ptr 取出值o->ptr = (void*)((long)value);}// ...return o;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241216195602245

  • key = “aaaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241216195527348

memory usage [key]

memory usage [key] 命令对应的处理函数为 memoryCommand

void memoryCommand(client *c) {// ...// 1.计算 value 的字节数size_t usage = objectComputeSize(c->argv[2],dictGetVal(de),samples,c->db->id);// 2.计算 key 的字节数usage += sdsZmallocSize(dictGetKey(de));// 3.计算键值对结构体 dictEntry 的字节数usage += sizeof(dictEntry);// 4.计算所在 db 库的字典元数据的字节数usage += dictMetadataSize(c->db->dict);// ...
}

usage 变量即是用于存储键值对的内存使用字节数。可以看到,共有四个部分组成:

  1. objectComputeSize(c->argv[2],dictGetVal(de),samples,c->db->id):计算 value 的字节数。
  2. sdsZmallocSize(dictGetKey(de)):计算 key 的字节数。
  3. sizeof(dictEntry):计算键值对结构体 dictEntry 的字节数。
  4. dictMetadataSize(c->db->dict):计算所在 db 库的字典元数据的字节数
🍀 计算 value 的字节数
size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {sds ele, ele2;dict *d;dictIterator *di;struct dictEntry *de;size_t asize = 0, elesize = 0, samples = 0;if (o->type == OBJ_STRING) {if(o->encoding == OBJ_ENCODING_INT) { // 执行第 1 个 if 中的语句// sizeof(struct redisObject) = 16 bytesasize = sizeof(*o);} else if(o->encoding == OBJ_ENCODING_RAW) {asize = sdsZmallocSize(o->ptr)+sizeof(*o);} else if(o->encoding == OBJ_ENCODING_EMBSTR) {asize = zmalloc_size((void *)o);} else {serverPanic("Unknown string encoding");}} else if (o->type == OBJ_LIST) {// ...} else if (o->type == OBJ_SET) {// ...} else if (o->type == OBJ_ZSET) {// ...} else if (o->type == OBJ_HASH) {// ...} else if (o->type == OBJ_STREAM) {// ...} else if (o->type == OBJ_MODULE) {// ...} else {serverPanic("Unknown object type");}return asize;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
  • key = “aaaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
🍀 计算 key 的字节数
size_t sdsZmallocSize(sds s) {// sds s 是 sds 结构体的 char buf[] 数组首元素地址,这里根据 s 获取 sds 结构体首地址void *sh = sdsAllocPtr(s);// jemalloc 根据首地址获取分配的连续内存块字节大小return zmalloc_size(sh);
}void *sdsAllocPtr(sds s) {// s 为 char buf[] 首元素地址// s[-1] 获取 type 成员地址,sdsHdrSize(s[-1]) 则是根据 type 获取 sds 结构体占用字节// 两者相减,就可以得到 sds 结构体首元素地址了return (void*) (s-sdsHdrSize(s[-1]));
}

对应的:

  • key = “aaaaaa”,通过之前对 sdsdup() 函数的分析,可得 jemalloc 实际为 key 分配了 8 字节的连续内存。
  • key = “aaaaaaa”,通过之前对 sdsdup() 函数的分析,可得 jemalloc 实际为 key 分配了 16 字节的连续内存。
🍀 计算键值对结构体 dictEntry 的字节数
typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;void *metadata[];
} dictEntry;

占用字节分析:

  • *void key:8 字节。
  • union v:联合体,8 字节。
  • *struct dictEntry next:8 字节。
  • void *metadata[]:柔性数组,不参与 sizeof(struct dictEntry) 计算。

综上,sizeof(struct dictEntry) 的结果为 24 字节。

🍀 计算所在 db 库的字典元数据的字节数
/** 返回 db 字典条目元数据的大小(以字节为单位)。* 在集群模式下,元数据用于构造属于同一集群槽的 dict 条目的双向链表。 */
size_t dictEntryMetadataSize(dict *d) {UNUSED(d);return server.cluster_enabled ? sizeof(clusterDictEntryMetadata) : 0;
}

在单机环境下,默认该函数的返回值为 0。

🍀 小结

综上对每个函数的分析,以及 set 时的具体实现,我们得出:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数816
计算键值对结构体 dictEntry 的字节数2424
计算所在 db 库的字典元数据的字节数00
字节总和4856

总结

造成差异的原因

通过上面对源码的分析,其实我们就可以知道 memory usage [key] 分析得到的内存使用情况为什么会有差异了。

首先需要说明的是,Redis 6.0 与 Redis 7.0 都为 key = "aaaaaaa" 都请求了 9 字节的内存字节大小,但 jemalloc 实际都分配了 16 字节的连续内存块,但是对于多出来的 7 字节却持有不同的态度。

  • Redis 6.0 中,不会将多分配的 7 字节作为 sds 结构体中的字符数组 buf 的空间使用,即会设置成员 alloc = initlen = 7
  • Redis 7.0 中,将多分配的 7 字节作为 sds 结构体中的字符数组 buf 的空间使用,即会设置成员 alloc = usable - hdrlen - 1 = 14

对应的在使用 memory usage [key] 计算内存占用时:

  • Redis 6.0 中,key 的字节数 = sdsHdrSize(s[-1]) + alloc + 1 = sds 结构体占用字节 + 为字符数组 buf 开辟了的内存大小 + 结束字符 ‘\0’ 1 个字节,即 9 个字节。
  • Redis 7.0 中,key 的字节数 = jemalloc 为 key 实际分配的连续内存块大小,即 16 个字节。

从这里我们可以看出,Redis 7.0 相较于 Redis 6.0,对于 jemalloc 实际分配的额外内存空间,进行了优化利用。

memory usage [key] 计算内存使用小结

Redis 6.0:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数89
计算键值对结构体 dictEntry 的字节数2424
字节总和4849

Redis 7.0:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数816
计算键值对结构体 dictEntry 的字节数2424
计算所在 db 库的字典元数据的字节数00
字节总和4856

感悟

最后,通过本文对源码的分析,我们可以认识到:

  1. Redis 使用 jemalloc 作为默认的内存分配器,这使得它能够更有效地管理内存分配。jemalloc 会根据请求的大小选择最合适的内存块,从而减少内部碎片并提高分配效率。
  2. 对于简单的数值型字符串,如果它们可以被表示为长整数(long),Redis 会选择使用 OBJ_ENCODING_INT 编码来节省空间。这种方式不仅减少了内存占用,而且加快了数据访问速度。
  3. 在设计数据结构时,考虑到字节对齐规则,以确保最佳性能,在本文分析中,在计算字节时并没有提到结构体字节对齐,这是因为 Redis 对数据结构的巧妙设计使得无需进行字节填充。此外,柔性数组用于 sds 结构体中,允许动态增长字符缓冲区而不增加额外的指针开销。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/63702.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实现 WebSocket 接入文心一言

目录 什么是 WebSocket&#xff1f; 为什么需要 WebSocket&#xff1f; HTTP 的局限性 WebSocket 的优势 总结&#xff1a;HTTP 和 WebSocket 的区别 WebSocket 的劣势 WebSocket 常见应用场景 WebSocket 握手过程 WebSocket 事件处理和生命周期 WebSocket 心跳机制 …

2024.7 XAI 遇见 LLM:可解释 AI 与大型语言模型之间关系的调查

https://arxiv.org/pdf/2407.15248 问题 Q1&#xff1a;XAI 技术当前如何与 LLMs 集成&#xff1f;Q2&#xff1a;将 LLMs 与 XAI 方法融合的新兴趋势是什么&#xff1f;Q3&#xff1a;当前相关文献存在哪些差距&#xff0c;哪些领域需要进一步研究&#xff1f; 挑战 LLMs …

RIP实验

要求及分析 路由器上分别配置环回 连接路由器的线路网段为12.1.1.0/24、23.1.1.1.0/24 R1和R3连接的网络地址分别为192.168.1.0/24/192.168.2.0/24 整个网络使用RIP达到全网可达 配置 先配置路由器各接口ip和环回和pc ip网关掩码&#xff08;图略&#xff09; 进行 RI…

Oracle 中间件 Webcenter Portal服务器环境搭建

环境信息 服务器基本信息 如下表&#xff0c;本次安装总共使用2台服务器&#xff0c;具体信息如下&#xff1a; Webcenter1服务器 归类 SOA服务器 Ip Address 172.xx.xx.xx.xx HostName wcc01.xxxxxx.com Alias wccprd01 Webcenter2服务器 归类 OSB服务器 Ip Addr…

macOS 配置 vscode 命令行启动

打开 vscode 使用 cmd shift p 组合快捷键&#xff0c;输入 install 点击 Install ‘code’ command in PATH Ref https://code.visualstudio.com/docs/setup/mac

分层架构 IM 系统之多媒体功能设计与实现

现在 IM 系统已经不仅限于文本消息的通讯了&#xff0c;多媒体数据占据越来越多的比重&#xff0c;比如&#xff1a;文件传输、语音通话、视频通话等。 在前面的文章&#xff08;《基于需求分析模型来结构化剖析 IM 系统》&#xff09;中我们分析过&#xff0c;“多媒体消息”…

0.gitlab ubuntu20.04 部署问题解决

安装依赖&#xff1a; ① sudo apt-get update 出现&#xff1a; 解决方式&#xff1a; 去 /etc/apt/sources.list.d 这个目录删除或注释对应的list文件 第三方软件的源一般都以list文件的方式放在 /etc/apt/sources.list.d 这个目录 重新运行sudo apt-get update 安装…

Next.js v15 - 服务器操作以及调用原理

约定 服务器操作是在服务器上执行的异步函数。它们可以在服务器组件和客户端组件中调用&#xff0c;用于处理 Next.js 应用程序中的表单提交和数据修改。 服务器操作可以通过 React 的 “use server” 指令定义。你可以将该指令放在 async 函数的顶部以将该函数标记为服务器操…

什么是3DEXPERIENCE SOLIDWORKS,它有哪些角色和功能?

将业界领先的 SOLIDWORKS 3D CAD 解决方案连接到基于单一云端产品开发环境 3DEXPERIENCE 平台。您的团队、数据和流程全部连接到一个平台进行高效的协作工作&#xff0c;从而能快速的做出更好的决策。 目 录&#xff1a; ★ 1 什么是3DEXPERIENCE SOLIDWORKS ★ 2 3DEXPERIE…

[Unity]【图形渲染】【游戏开发】Shader数学基础4-更多矢量运算

在计算机图形学和着色器编程中,矢量运算是核心的数学工具之一。矢量用于描述空间中的位置、方向、速度等各种物理量,并在图形变换、光照计算、纹理映射等方面起着至关重要的作用。本篇文章将详细讲解矢量和标量之间的乘法与除法、矢量的加法与减法、矢量的模与单位矢量、点积…

【漏洞复现】CVE-2023-37461 Arbitrary File Writing

漏洞信息 NVD - cve-2023-37461 Metersphere is an opensource testing framework. Files uploaded to Metersphere may define a belongType value with a relative path like ../../../../ which may cause metersphere to attempt to overwrite an existing file in the d…

Bcrypt在线密码加密生成器

具体前往&#xff1a;在线Bcrypt加密工具--使用bcrypt及生成salt的迭代次数强度参数计算生成哈希(摘要)

wxWidgets使用wxStyledTextCtrl(Scintilla编辑器)的正确姿势

开发CuteMySQL/CuteSqlite开源客户端的时候&#xff0c;需要使用Scintilla编辑器&#xff0c;来高亮显示SQL语句&#xff0c;作为C/C领域最成熟稳定又小巧的开源编辑器&#xff0c;Scintilla提供了强大的功能&#xff0c;wxWidgets对Scintilla进行包装后的是控件类&#xff1a;…

构建高性能异步任务引擎:FastAPI + Celery + Redis

在现代应用开发中&#xff0c;异步任务处理是一个常见的需求。无论是数据处理、图像生成&#xff0c;还是复杂的计算任务&#xff0c;异步执行都能显著提升系统的响应速度和吞吐量。今天&#xff0c;我们将通过一个实际项目&#xff0c;探索如何使用 FastAPI、Celery 和 Redis …

【win10+RAGFlow+Ollama】搭建本地大模型助手(教程+源码)

一、RAGFlow简介 RAGFlow是一个基于对文档深入理解的开源RAG&#xff08;Retrieval-augmented Generation&#xff0c;检索增强生成&#xff09;引擎。 主要作用&#xff1a; 让用户创建自有知识库&#xff0c;根据设定的参数对知识库中的文件进行切块处理&#xff0c;用户向大…

C/C++圣诞树

系列文章 序号直达链接1C/C爱心代码2C/C跳动的爱心3C/C李峋同款跳动的爱心代码4C/C满屏飘字表白代码5C/C大雪纷飞代码6C/C烟花代码7C/C黑客帝国同款字母雨8C/C樱花树代码9C/C奥特曼代码10C/C精美圣诞树11C/C俄罗斯方块12C/C贪吃蛇13C/C孤单又灿烂的神-鬼怪14C/C闪烁的爱心15C…

投标心态:如何在“标海战术”中保持清醒的头脑?

在竞争激烈的市场环境下&#xff0c;“标海战术”——即大规模参与投标——已经成为许多企业争取市场份额的重要策略。然而&#xff0c;盲目追求投标数量可能导致资源浪费、团队疲劳以及战略目标的模糊化。在这种高强度的竞争模式中&#xff0c;如何保持清醒的头脑&#xff0c;…

研发效能DevOps: Vite 使用 Element Plus

目录 一、实验 1.环境 2.初始化前端项目 3.安装 vue-route 4.安装 pinia 5.安装 axios 6.安装 Element Plus 7.gitee创建工程 8. 配置路由映射 9.Vite 使用 Element Plus 二、问题 1.README.md 文档推送到gitee未自动换行 2.访问login页面显示空白 3.表单输入账户…

NVIDIA DeepStream插件之Gst-nvtracker

NVIDIA DeepStream插件之Gst-nvtracker 1. 源由2. 基础知识3. Gst-nvtracker插件3.1 插件参数3.2 插件API接口 4. 分析问题5. 总结6. 参考资料 1. 源由 这篇的主要目的是稍微吐槽下NVIDIA的设计&#xff0c;当然其实他们做的还是不错的&#xff08;从系统架构设计角度看&#…

进程内存转储工具|内存镜像提取-取证工具

1.内存转储&#xff0c;内存转储&#xff08;Memory Dump&#xff09;是将计算机的物理内存&#xff08;RAM&#xff09;内容复制到一个文件中的过程&#xff0c;这个文件通常被称为“内存转储文件”或“核心转储文件”&#xff08;Core Dump&#xff09;,内存转储的主要目的是…