主頁 >  其他 > 【原始碼講解】Redis中記憶體優化的資料結構是如何設計的

【原始碼講解】Redis中記憶體優化的資料結構是如何設計的

2021-10-13 08:17:01 其他

文章目錄

  • 優化的資料結構
    • SDS 設計
      • redisObject 結構體
      • 嵌入式字串
    • ziplist 設計
    • intset 設計
  • 共享物件

Redis 作為記憶體資料庫,如何高效地使用記憶體非常重要,為了提升記憶體的使用率,主要采取資料結構優化設計及使用以及記憶體資料按照規則淘汰,記憶體資料按照淘汰規則主要通過 Redis 的記憶體替換策略實作的,也就是將很少使用的記憶體資料淘汰,這樣就可以更好地把記憶體空間給頻繁使用的資料使用,
下面,我們就通過原始碼講解下 Redis 中記憶體友好的資料結構是如何設計的以及Redis 如何優化記憶體使用的,

優化的資料結構

在 Redis 中,有三種資料結構針對記憶體使用率做了設計上的優化,分別是動態字串(SDS)、壓縮串列(ziplist)和整數集合(intset),下面我們來學習下,

SDS 設計

字串在平常應用開發中使用的非常頻繁,對于 Redis 來說,鍵值中的鍵是字串,值有時也是字串,而且客戶端和服務器之間互動的命令和資料也是字串表示的,既然字串的使用那么廣泛和關鍵,在實作字串時,應該滿足以下條件:

  • 支持豐富的字串操作(比如追加、洗掉、比較、獲取長度等操作)
  • 能保存任意的二進制資料
  • 可以節省記憶體開銷

在 C 語言中經常使用 char* 字符陣列來實作字串,同時,C 語言標準庫 string.h 也定義了字串操作函式,比如獲取字串長度的 strlen 等,從而方便使用者操作字串,這樣感覺是可以復用 C 語言的字串操作的實作了,但是 C 語言中需要頻繁手動檢查和分配字串的記憶體空間,從而增加了代碼開發作業量,而且很多二進制資料還無法用字串保存,也限制了使用范圍,

Redis 從系統設計角度上來說,設計 SDS(Simple Dynamic String)結構,用來表示字串,同 C 語言的字串比較,它會提升字串的操作效率,并且可以保存二進制資料,這部分內容我會在之后的文章中講解,為了避免記憶體浪費,SDS 設計了 不同型別的結構頭,主要包括 sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,它們可以適配不同大小的字串,

SDS 除了使用設計精巧的結構頭外,在保存較小字串時,其實還用了嵌入式字串,這樣避免了給字串分配額外的空間,可以讓字串直接保存在 Redis 的基本資料物件結構中,

所以,接下來我們趕緊來理解下,Redis 使用的基本資料物件結構體 redisObject 是啥樣的?

redisObject 結構體

redisObject 是定義在 server.h 檔案中的,其主要功能是保存鍵值對中的值,這個結構一共定義了 4 個元資料和一個指標,

typedef struct redisObject {
    //redisObject 資料型別,4bits
    unsigned type:4;
    //redisObject 的編碼型別,4bits
    unsigned encoding:4;
    //redisObject 的 LRU 時間,LRU_BITS 占用 24 個 bits
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    //redisObject 的參考技術
    int refcount;
    //指向值的指標
    void *ptr;
} robj;

從 redisObject 結構體定義中我們可以看到,在 type、encoding、lru 這三個變數后面都跟著一個冒號,冒號后緊跟著一個數值表示該元資料占用的位元數,其中 type 和 encoding 占用 4 位元,而 lru 占用的位元數是有 server.h 中的宏定義的,默認是 24 位元,

#define LRU_BITS 24

在這里變數后使用冒號和數值的定義方法,實際上是 C 語言的位域定義方法,這種方法可以節省記憶體開銷,當一個變數占用不了一個資料型別的所有 bits 時就采用位域的方法,把一個資料型別中的 bits 劃分為多個位域,每個位域占用一定的位元數,這樣一個資料型別的所有 bits 可以定義多個變數節省了記憶體開銷,對于 type、encoding、lru 都是 unsigned 的,unsigned 本身 4 個位元組,使用位域的方法后節省了 8 個位元組,下面我們再繼續了解下嵌入式字串是如何實作的,

嵌入式字串

SDS 在保存較小的字串時,會使用嵌入式字串,直接將字串保存在結構體 redisObject 中,redisObject 結構中的指標 ptr 指向值的資料結構,比如創建一個 String 型別的值,Redis 會呼叫 createStringObject 函式來創建對應的 redisObject、redisObject 結構中的 ptr 指標就會指向 SDS 結構,createStringObject 函式中會根據字串長度來選擇呼叫哪個函式,原始碼如下:

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    //創建嵌入式字串,長度小于 44 位元組
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    //創建普通字串,長度大于 44 位元組
    else
        return createRawStringObject(ptr,len);
}

對于 createRawStringObject 函式來說,它在創建 String 型別的值時呼叫 createObject 函式,createObject 函式主要用來創建 Redis 的資料物件的,Redis 中資料物件型別比較多,(比如 String、List、Hash 等),因此 createObject 函式包含兩個引數,第一個是用來表示索要創建的資料物件型別,第二個引數是指向資料物件的指標,來看下 createRawStringObject 原始碼看看是如何呼叫 createObject 函式的,

robj *createRawStringObject(const char *ptr, size_t len) {
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

可以看到在呼叫 createObject 函式時,會呼叫 OBJ_STRING 型別,它表示要創建 String 型別的物件,指向 SDS 結構的指標都是由 sdsnewlen 函式回傳的,而 sdsnewlen 函式正式用來創建 SDS 結構的,

sds sdsnewlen(const void *init, size_t initlen) {
    return _sdsnewlen(init, initlen, 0);
}

接下來,我們繼續看看 createObject 函式的原始碼:

robj *createObject(int type, void *ptr) {
    //為 redisObject 分配記憶體空間
    robj *o = zmalloc(sizeof(*o));
    //指定 redisObject 的型別
    o->type = type;
    //指定 redisObject 的編碼型別,這里表示普通的 SDS
    o->encoding = OBJ_ENCODING_RAW;
    //指標直接傳入給 ptr 成員
    o->ptr = ptr;
    o->refcount = 1;

    /* Set the LRU to the current lruclock (minutes resolution), or
     * alternatively the LFU counter. */
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;
}

也就是說創建普通的 SDS 時,需要給 redisObject 和 SDS 分配記憶體,這樣就會遭成極大的記憶體分配開銷和記憶體碎片,所以當 SDS 小于和等于 44 位元組時,Redis 創建嵌入式字串來減少不必要的記憶體分配開銷和記憶體碎片,要創建嵌入式字串就得呼叫 createEmbeddedStringObject 函式,我們來看下這個函式的原始碼:

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
   //sh 指向 SDS 結構頭起始位置 
   struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    //移動 ptr 指標到結構頭末尾,字符陣列起始位置
    o->ptr = sh+1;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        //ptr 指向的字串拷貝到 SDS 的字符陣列中
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

在代碼中可以看到首先分配了一塊連續的記憶體區域,該區域包含 redisObject 大小、SDS 結構頭 sdshdr8 大小以及字串大小總和,最后加 1 表示加上了字串結束符 \0,記憶體分配完后,則創建 SDS 結構頭指標 sh,sh 指向這塊連續記憶體區域 SDS 結構頭所在的位置,o+1 表示從記憶體區域開始位置移動了 redisObject 大小的位置,接著 redisObject 中 ptr 指標將指向 SDS 的字符陣列,其中 sh 就是剛才說的 SDS 結構頭指標,指向 sdshdr8 型別,sh+1 表示把地址從 sh 開始移動 sdshdr8 大小的距離,這時候 ptr 指標指向了 SDS 結構頭的末尾,字符陣列的起始位置處,最后把 ptr 指向的字串拷貝給到結構 SDS 中的字符陣列,并在陣列尾部添加結束符 \0

通過代碼實作看到 Redis 把 redisObject 和 SDS 一起放在了一塊連續的記憶體空間中,對于大小不超過 44 位元組的字串來說不容易遭成記憶體分配開銷和記憶體碎片,除了嵌入式字串設計外,Redis 還設計了壓縮串列(ziplist)和整數集合(intset)這兩種類似的資料結構,

ziplist 設計

在 Redis 中,List、Hash 以及 Sorted Set 都可以用壓縮串列(ziplist)來保存,下面我們首先通過它的創建函式來看下如何實作的,源代碼如下:

unsigned char *ziplistNew(void) {
    //初始分配的大小
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    zl[bytes-1] = ZIP_END;
    return zl;
}

這個代碼首先分配一塊大小為 ZIPLIST_HEADER_SIZE 和 ZIPLIST_END_SIZE 總和的記憶體區域,然后最后一個位元組用 ZIP_END 來賦值表示串列結束,那么宏 ZIPLIST_HEADER_SIZE 和 ZIPLIST_END_SIZE 又是多少呢?我們來看下:

//ziplist 的串列頭大小,分別表示壓縮串列總共的位元組數,最后一個元素離頭部的偏移、串列元素個數
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
//ziplist 的串列尾大小,表示串列結束
/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

所以在創建一個空的壓縮串列的時候,其記憶體布局如下:

? 32bit32bit16bit8bit
ziplist 總位元組數最后一個元素離頭部的偏移ziplist 包含的元素個數255

當我們往壓縮串列插入資料的時候,ziplist 會根據不同的資料型別(字串、整數)以及它們的大小來進行編碼,這種方式起到了節省記憶體的作用,下面我們就來看看它們是如何編碼的,

要了解 ziplist 是如何編碼的,我們得首先來了解 ziplist 的 entry,它主要包含前一項的長度(prevlen),當前項長度的編碼(encoding),以及當前項的實際資料(data),

在這里插入圖片描述

這里的編碼指的是用不同數量的位元組來表示保存的資訊,ziplist 中包含多個串列項,每個串列項都是緊挨著的,為了方便查找每個串列項都會包含前一項的長度,因為每個串列長度不一樣,如果都統一使用相同大小來記錄 prevlen 將遭成記憶體空間浪費,比如,某個串列項只是一個字串“hello”,包含 5 個位元組,1 個位元組可以表達 256 位元組的字串,此時 prevlen 有 3 個位元組是被浪費掉的,Redis 在對 prevlen 進行編碼時就先呼叫 zipStorePrevEntryLength 函式,函式原始碼如下:

unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
    if (p == NULL) {
        return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(uint32_t) + 1;
    } else {
        //如果 len 小于 254 位元組,那么回傳 prevlen 為 1 個位元組
        if (len < ZIP_BIG_PREVLEN) {
            p[0] = len;
            return 1;
        } else {
            return zipStorePrevEntryLengthLarge(p,len);
        }
    }
}

該函式中用于判斷一個串列項是否小于 254 位元組,如果小于 254 位元組 prevlen 就用 1 個位元組表示,否則就用 zipStorePrevEntryLengthLarge 函式進一步編碼, zipStorePrevEntryLengthLarge 函式原始碼如下:

int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
    uint32_t u32;
    if (p != NULL) {
      // 將 prevlen 的第一個位元組設定為 ZIP_BIG_PREVLEN
        p[0] = ZIP_BIG_PREVLEN;
        u32 = len;
      // 將前一個串列項的長度值拷貝到第 2 到第 5 個位元組
        memcpy(p+1,&u32,sizeof(u32));
        memrev32ifbe(p+1);
    }
    // 回傳 prevlen 的大小
    return 1 + sizeof(uint32_t);
}

會先將 prevlen 第一個位元組設定成 ZIP_BIG_PREVLEN,這個宏的值就是 254,然后使用 memcpy 將前一個串列項的長度拷貝到第 2 到第 5 個位元組,所以 prevlen 有 1 位元組和 5 位元組兩種方式編碼,這種方式其實是根據不同長度的資料,使用不同大小的元資料資訊,

intset 設計

整數集合(intset)是資料型別集合 Set 的底層實作,和嵌入式字串和 ziplist 類似,整數集合也是用了一塊連續的記憶體空間,下面我們來看看整數集合的定義,

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

其中資料部分就是一個 int8_t 的整形陣列 contents,一般來說整形陣列是作為一塊連續的記憶體空間的,所以這樣也同樣避免了記憶體碎片從而提高了記憶體使用率,除此以外,為了節省記憶體開銷,Redis 在記憶體訪問上還采取了共享物件的方式,

共享物件

Redis 的實體運行時,會遇到有些經常訪問的資料,比如常見的整數或者 Redis 的回復資訊 OK、ERR,以及報錯資訊,因此,為了避免反復創建經常訪問的資料,Redis 采用了共享物件的方式,就是把常用資料創建為共享物件,上層應用訪問時只需要直接讀取,比如現在有 2000 個用戶同時保存”40“這個物件,如果 Redis 為每一個用戶都為 40 創建 redisObject 將遭成記憶體大量的浪費,采用共享物件的方式記憶體中只保存一個 40 的 redisObject,節省了記憶體空間,創建共享物件需要使用 server.c 中的 createSharedObjects 函式,該函式定義如下:

void createSharedObjects(void) {
    int j;

    /* 常見的回復資訊 */
    shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
    shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
    shared.emptybulk = createObject(OBJ_STRING,sdsnew("$0\r\n\r\n"));
    shared.czero = createObject(OBJ_STRING,sdsnew(":0\r\n"));
    shared.cone = createObject(OBJ_STRING,sdsnew(":1\r\n"));
    shared.emptyarray = createObject(OBJ_STRING,sdsnew("*0\r\n"));
    shared.pong = createObject(OBJ_STRING,sdsnew("+PONG\r\n"));
    shared.queued = createObject(OBJ_STRING,sdsnew("+QUEUED\r\n"));
    shared.emptyscan = createObject(OBJ_STRING,sdsnew("*2\r\n$1\r\n0\r\n*0\r\n"));
    shared.space = createObject(OBJ_STRING,sdsnew(" "));
    shared.colon = createObject(OBJ_STRING,sdsnew(":"));
    shared.plus = createObject(OBJ_STRING,sdsnew("+"));

    /* Shared command error responses */
    shared.wrongtypeerr = createObject(OBJ_STRING,sdsnew(
        "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"));
    shared.err = createObject(OBJ_STRING,sdsnew("-ERR\r\n"));
    shared.nokeyerr = createObject(OBJ_STRING,sdsnew(
        "-ERR no such key\r\n"));
    shared.syntaxerr = createObject(OBJ_STRING,sdsnew(
        "-ERR syntax error\r\n"));
    shared.sameobjecterr = createObject(OBJ_STRING,sdsnew(
        "-ERR source and destination objects are the same\r\n"));
    shared.outofrangeerr = createObject(OBJ_STRING,sdsnew(
        "-ERR index out of range\r\n"));
    shared.noscripterr = createObject(OBJ_STRING,sdsnew(
        "-NOSCRIPT No matching script. Please use EVAL.\r\n"));
    shared.loadingerr = createObject(OBJ_STRING,sdsnew(
        "-LOADING Redis is loading the dataset in memory\r\n"));
    shared.slowscripterr = createObject(OBJ_STRING,sdsnew(
        "-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.\r\n"));
    shared.masterdownerr = createObject(OBJ_STRING,sdsnew(
        "-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.\r\n"));
    shared.bgsaveerr = createObject(OBJ_STRING,sdsnew(
        "-MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.\r\n"));
    shared.roslaveerr = createObject(OBJ_STRING,sdsnew(
        "-READONLY You can't write against a read only replica.\r\n"));
    shared.noautherr = createObject(OBJ_STRING,sdsnew(
        "-NOAUTH Authentication required.\r\n"));
    shared.oomerr = createObject(OBJ_STRING,sdsnew(
        "-OOM command not allowed when used memory > 'maxmemory'.\r\n"));
    shared.execaborterr = createObject(OBJ_STRING,sdsnew(
        "-EXECABORT Transaction discarded because of previous errors.\r\n"));
    shared.noreplicaserr = createObject(OBJ_STRING,sdsnew(
        "-NOREPLICAS Not enough good replicas to write.\r\n"));
    shared.busykeyerr = createObject(OBJ_STRING,sdsnew(
        "-BUSYKEY Target key name already exists.\r\n"));

    /* The shared NULL depends on the protocol version. */
    shared.null[0] = NULL;
    shared.null[1] = NULL;
    shared.null[2] = createObject(OBJ_STRING,sdsnew("$-1\r\n"));
    shared.null[3] = createObject(OBJ_STRING,sdsnew("_\r\n"));

    shared.nullarray[0] = NULL;
    shared.nullarray[1] = NULL;
    shared.nullarray[2] = createObject(OBJ_STRING,sdsnew("*-1\r\n"));
    shared.nullarray[3] = createObject(OBJ_STRING,sdsnew("_\r\n"));

    shared.emptymap[0] = NULL;
    shared.emptymap[1] = NULL;
    shared.emptymap[2] = createObject(OBJ_STRING,sdsnew("*0\r\n"));
    shared.emptymap[3] = createObject(OBJ_STRING,sdsnew("%0\r\n"));

    shared.emptyset[0] = NULL;
    shared.emptyset[1] = NULL;
    shared.emptyset[2] = createObject(OBJ_STRING,sdsnew("*0\r\n"));
    shared.emptyset[3] = createObject(OBJ_STRING,sdsnew("~0\r\n"));

    for (j = 0; j < PROTO_SHARED_SELECT_CMDS; j++) {
        char dictid_str[64];
        int dictid_len;

        dictid_len = ll2string(dictid_str,sizeof(dictid_str),j);
        shared.select[j] = createObject(OBJ_STRING,
            sdscatprintf(sdsempty(),
                "*2\r\n$6\r\nSELECT\r\n$%d\r\n%s\r\n",
                dictid_len, dictid_str));
    }
    shared.messagebulk = createStringObject("$7\r\nmessage\r\n",13);
    shared.pmessagebulk = createStringObject("$8\r\npmessage\r\n",14);
    shared.subscribebulk = createStringObject("$9\r\nsubscribe\r\n",15);
    shared.unsubscribebulk = createStringObject("$11\r\nunsubscribe\r\n",18);
    shared.psubscribebulk = createStringObject("$10\r\npsubscribe\r\n",17);
    shared.punsubscribebulk = createStringObject("$12\r\npunsubscribe\r\n",19);

    /* Shared command names */
    shared.del = createStringObject("DEL",3);
    shared.unlink = createStringObject("UNLINK",6);
    shared.rpop = createStringObject("RPOP",4);
    shared.lpop = createStringObject("LPOP",4);
    shared.lpush = createStringObject("LPUSH",5);
    shared.rpoplpush = createStringObject("RPOPLPUSH",9);
    shared.lmove = createStringObject("LMOVE",5);
    shared.blmove = createStringObject("BLMOVE",6);
    shared.zpopmin = createStringObject("ZPOPMIN",7);
    shared.zpopmax = createStringObject("ZPOPMAX",7);
    shared.multi = createStringObject("MULTI",5);
    shared.exec = createStringObject("EXEC",4);
    shared.hset = createStringObject("HSET",4);
    shared.srem = createStringObject("SREM",4);
    shared.xgroup = createStringObject("XGROUP",6);
    shared.xclaim = createStringObject("XCLAIM",6);
    shared.script = createStringObject("SCRIPT",6);
    shared.replconf = createStringObject("REPLCONF",8);
    shared.pexpireat = createStringObject("PEXPIREAT",9);
    shared.pexpire = createStringObject("PEXPIRE",7);
    shared.persist = createStringObject("PERSIST",7);
    shared.set = createStringObject("SET",3);
    shared.eval = createStringObject("EVAL",4);

    /* Shared command argument */
    shared.left = createStringObject("left",4);
    shared.right = createStringObject("right",5);
    shared.pxat = createStringObject("PXAT", 4);
    shared.px = createStringObject("PX",2);
    shared.time = createStringObject("TIME",4);
    shared.retrycount = createStringObject("RETRYCOUNT",10);
    shared.force = createStringObject("FORCE",5);
    shared.justid = createStringObject("JUSTID",6);
    shared.lastid = createStringObject("LASTID",6);
    shared.default_username = createStringObject("default",7);
    shared.ping = createStringObject("ping",4);
    shared.setid = createStringObject("SETID",5);
    shared.keepttl = createStringObject("KEEPTTL",7);
    shared.load = createStringObject("LOAD",4);
    shared.createconsumer = createStringObject("CREATECONSUMER",14);
    shared.getack = createStringObject("GETACK",6);
    shared.special_asterick = createStringObject("*",1);
    shared.special_equals = createStringObject("=",1);
    shared.redacted = makeObjectShared(createStringObject("(redacted)",10));

    for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
        shared.integers[j] =
            makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
        shared.integers[j]->encoding = OBJ_ENCODING_INT;
    }
    for (j = 0; j < OBJ_SHARED_BULKHDR_LEN; j++) {
        shared.mbulkhdr[j] = createObject(OBJ_STRING,
            sdscatprintf(sdsempty(),"*%d\r\n",j));
        shared.bulkhdr[j] = createObject(OBJ_STRING,
            sdscatprintf(sdsempty(),"$%d\r\n",j));
    }
    /* The following two shared objects, minstring and maxstrings, are not
     * actually used for their value but as a special object meaning
     * respectively the minimum possible string and the maximum possible
     * string in string comparisons for the ZRANGEBYLEX command. */
    shared.minstring = sdsnew("minstring");
    shared.maxstring = sdsnew("maxstring");
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/310567.html

標籤:其他

上一篇:【鴻蒙征程】三.?終于肝出了鴻蒙組態檔,資源檔案的思維導圖?

下一篇:資料結構入門:時間復雜度

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more