Redis源碼剖析--字符串t_string

前面一直在分析Redis的底層數據結構,Redis利用這些底層結構設計了它面向用戶可見的五種數據結構,字符串、哈希,鏈表,集合和有序集合,然後用redisObject對這五種結構進行了封裝。從這篇博客開始,帶你一點點分析五種數據類型常見命令對應的源碼實現,慢慢地解開Redis的面紗。

字符串概述

字符串是Redis中最爲常見的數據存儲類型,其底層實現是簡單動態字符串sds,因此,該字符串類型是二進制安全的,這就意味着它可以接受任何格式的數據。另外,Redis規定,字符串類型最多可以容納的數據長度爲512M。Redis提供了下列函數,來檢測字符串鍵的大小。

static int checkStringLength(client *c, long long size) {
    // 超出了512M,就直接報錯
    if (size > 512*1024*1024) {
        addReplyError(c,"string exceeds maximum allowed size (512MB)");
        return C_ERR;
    }
    return C_OK;
}

字符串命令

Redis爲string提供了一系列的命令,用來操作和管理字符串,主要包括以下幾個命令。

命令 命令描述
SET key value [ex 秒數][px 毫秒數][nx/xx] 設置指定key的值
GET key 獲取指定key的值
APPEND key value 將value追加到指定key的值末尾
INCRBY key increment 將指定key的值加上增量increment
DECRBY key decrement 將指定key的值減去增量decrement
STRLEN key 返回指定key的值長度
SETRANGE key offset value 將value覆寫到指定key的值上,從offset位開始
GETRANGE key start end 獲取指定key中字符串的子串[start,end]
MSET key value [key value …] 一次設定多個key的值
MGET key1 [key2..] 一次獲取多個key的值

上述命令均爲常用的字符串命令,其實現在t_string.c文件中,我們進而來查看一下它們的實現源碼。

void setCommand(client *c); // SET命令,設定鍵值對
void setnxCommand(client *c); // SETNX命令,key不存在時才設置值
void setexCommand(client *c); // SETEX命令,key存在時才設置值,到期時間爲秒
void psetexCommand(redisClient *c) // PSETEX命令,key存在時才設置值,到期時間爲毫秒
void setrangeCommand(client *c); // SETRANGE命令,範圍性的設置值
void msetCommand(client *c); // MSET命令,一次設定對個鍵值對
void msetnxCommand(client *c); // MSETNX命令,key不存在時才設置值
void getCommand(client *c); // GET命令,獲取key對應的value
void mgetCommand(client *c); // MGET命令,獲取多個key對應的value
void getrangeCommand(client *c); // GETRANGE命令,範圍性的獲取值
void getsetCommand(client *c); // 獲取指定的鍵,如果存在則修改其值;反之不進行操作
void incrCommand(client *c); // 值遞增1操作
void decrCommand(client *c); // 值遞減1操作
void incrbyCommand(client *c); // 值增加操作
void decrbyCommand(client *c); // 值減少操作
void appendCommand(redisClient *c) // 追加key對應的值 
void strlenCommand(redisClient *c) // 獲取key對應值得長度

接下來,我們以SET命令爲例,來理解以下Redis處理字符串命令的過程。

例:SET命令實現流程

set命令用於設置指定的值,其具體命令格式如下:

set key value [ex 秒數] [px 毫秒數] [nx/xx]

其中,各個選項的含義如下:

  • ex 設置指定的到期時間,單位爲秒
  • px 設置指定的到期時間,單位爲毫秒
  • nx 只有在key不存在的時候,才設置key的值
  • xx 只有key存在時,纔對key進行設置操作

例如,我們在Redis的客戶端中輸入:

127.0.0.1:6379> set zee 100 ex 1000 nx
OK
// 代表設定一組鍵值對[zee,100],其中,到期時間爲1000秒,如果zee不存在則創建key並設定值

SET 命令的源碼由setcommod函數實現,調用set命令需要傳入一個client的指針,client類型裏面包含了很多Redis對於交互命令的處理參數,我們沒必要去管一些目前還用不上的參數,先來看看set命令需要用到的參數。

typedef struct client {
    redisDb *db;            // 當前數據庫
    robj **argv;            // 命令參數
    // ....
} client;

很顯然,db指向一個我們當前需要操作的數據庫,argv指向待傳入的命令參數。當我們執行set zee 100 ex 1000 nx命令時,argv中就包含六個RedisObject結構,其對應如下:

argv[0] -- set
argv[1] -- zee
argv[3] -- 100
argv[4] -- ex
argv[5] -- 1000
argv[6] -- nx

我們規定了到期時間爲1000秒,且只有在zee鍵不存在的時候才設定該鍵的值。Redis爲SET命令的操作設定了下列三個宏定義,用來標記SET的操作類型。

// 關於set命令的操作有三種宏定義
#define OBJ_SET_NO_FLAGS 0    // 沒有設定參數
#define OBJ_SET_NX (1<<0)     // 只有鍵不存在時才設定其值
#define OBJ_SET_XX (1<<1)      // 只有鍵存在時才設定其值
#define OBJ_SET_EX (1<<2)       // ex屬性,到期時間單位爲秒
#define OBJ_SET_PX (1<<3)       // px屬性,到期時間單位爲毫秒

有了上述的理解之後,我們可以進入setCommand函數了。

/* set命令實現函數 */
void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    // 用於標記ex/px和nx/xx命令參數
    int flags = OBJ_SET_NO_FLAGS;
    // 從命令串的第四個參數開始,查看其是否設定了ex/px和nx/xx
    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
            !(flags & OBJ_SET_XX)) // 標記
        {
            flags |= OBJ_SET_NX;
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_NX))
        {
            flags |= OBJ_SET_XX;
        } else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_PX) && next)
        {
            flags |= OBJ_SET_EX;
            unit = UNIT_SECONDS;
            expire = next;
            j++;
        } else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_EX) && next)
        {
            flags |= OBJ_SET_PX;
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            // 如果不是上述參數,則需要報錯,命令錯誤
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    // 判斷value是否可以編碼成整數,如果能則編碼;反之不做處理
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    // 調用底層函數進行鍵值對設定
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
/* 真正的set底層實現函數 */
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */
    // 設定過期時間
    if (expire) {
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
        if (milliseconds <= 0) {
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }
    // 判斷key是否存在,並根據nx和xx命令來決定是否set命令是否執行
    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.nullbulk);
        return;
    }
    // 將鍵值對關聯到數據庫
    setKey(c->db,key,val);
    // 設定該數據庫爲髒
    server.dirty++;
    // 設定過期時間
    if (expire) setExpire(c->db,key,mstime()+milliseconds);
    // 發送事件通知
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
    // 發送定期事件通知
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
        "expire",key,c->db->id);
    // 向客戶端發送命令處理結果
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

從SET命令中,衍生除了SETNX,SETEX,PSETEX等命令,其底層均是調用setGenericCommand來實現。

// key不存在時,才設定值,flag爲REDIS_SET_NX;如果key存在則不做處理
// 命令形式爲:setnx key value
void setnxCommand(client *c) {
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,OBJ_SET_NX,c->argv[1],c->argv[2],NULL,0,shared.cone,shared.czero);
}
// key存在時才設置值,flag爲REDIS_SET_NO_FLAGS,過期時間單位爲秒,如果key不存在則不做處理
// 命令形式爲:setex key seconds value (seconds爲鍵過期時間,單位秒)
void setexCommand(client *c) {
    // 這裏爲argv[3],因爲value存放在此
    c->argv[3] = tryObjectEncoding(c->argv[3]);
    setGenericCommand(c,OBJ_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_SECONDS,NULL,NULL);
}
// key存在時才設置值,flag爲REDIS_SET_NO_FLAGS,過期時間單位爲毫秒
// PSETEX key milliseconds value(milliseconds爲鍵過期時間,單位毫秒)
void psetexCommand(client *c) {
    c->argv[3] = tryObjectEncoding(c->argv[3]);
    setGenericCommand(c,OBJ_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_MILLISECONDS,NULL,NULL);
}

字符串小結

在字符串的處理命令中,涉及到很多數據庫和事件的相關處理函數,現階段我們可以忽略。本博客只是列舉了set命令的源碼處理過程,這些命令的處理大多是涉及到命令解析的過程,比較繁瑣,但是很好理解。有興趣的可以在深入到每個命令的源碼中,一窺實現步驟。源碼面前,了無祕密。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章