不同版本的 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.pswp.cn/web/63702.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/63702.shtml
英文地址,請注明出處:http://en.pswp.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 …

前端滾動錨點(點擊后頁面滾動到指定位置)

三個常用方案&#xff1a; 1.scrollintoView 把調用該方法的元素滾動到屏幕的指定位置&#xff0c;中間&#xff0c;底部&#xff0c;或者頂部 優點&#xff1a;方便&#xff0c;只需要獲取元素然后調用 缺點&#xff1a;不好精確控制&#xff0c;只能讓元素指定滾動到中間&…

前端筆記——大數據量瀏覽器卡頓優化思路

多任務數據量處理卡頓問題 任務分批次 為避免阻塞&#xff0c;可以將 長時間的單一任務 拆分成多個小任務并分批執行。這樣可以在兩次任務之間讓瀏覽器有時間處理渲染、用戶輸入等操作。兩種常見方法&#xff1a; setTimeout 方法&#xff1a; 使用 setTimeout 將任務分段&a…

數智化轉型是什么?

數智化轉型是指企業通過數字化&#xff08;Digitalization&#xff09;和智能化&#xff08;Intelligentization&#xff09;技術的結合&#xff0c;推動業務流程、產品服務、組織管理的全面升級&#xff0c;從而提升效率、增強創新能力&#xff0c;并實現更高價值。相比傳統的…

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

3、交換機IP路由功能

每個用例前自己最好先畫個圖&#xff0c;不然容易繞暈&#xff0c;這篇文章寫好久了&#xff0c;自己都覺得有點繞 一、直連路由 如果一個交換機與另一個交換機時直連著的并且他們用來連接的端口屬于同網段&#xff0c;那么這種情況下他們就屬于直連路由。不需要做任何配置便可…

分層架構 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…

【華為OD-E卷-開心消消樂 100分(python、java、c++、js、c)】

【華為OD-E卷-開心消消樂 100分&#xff08;python、java、c、js、c&#xff09;】 題目 給定一個 N 行 M 列的二維矩陣&#xff0c;矩陣中每個位置的數字取值為 0 或 1。矩陣示例如&#xff1a; 1 1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 現需要將矩陣中所有的 1 進行反轉為 0&#…

[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的迭代次數強度參數計算生成哈希(摘要)

Django 模板分割及多語言支持案例【需求文檔】-->【實現方案】

Django 模板分割及多語言支持案例 這個案例旨在提供一個清晰的示范&#xff0c;展示如何將復雜的頁面分解為多個可復用的模板組件&#xff0c;使代碼更加模塊化和易于管理。希望這篇案例文章對你有所幫助。 概述 在 Django 項目開發中&#xff0c;使用模板分割和多語言支持能…

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 …