《redis設計與實現》2-資料庫實現篇

kinnylee發表於2018-12-17

上一篇文章介紹了redis基本的資料結構和物件《redis設計與實現》1-資料結構與物件篇

本文主要關於:

  • redis資料庫實現的介紹
  • 前面介紹的各種資料,在redis伺服器中的記憶體模型是什麼樣的的。
  • RDB檔案將這些記憶體資料持久化後的格式是什麼樣的
  • RDB和AOF序列化的區別是什麼
  • redis提供什麼機制保障AOF檔案不會一直增長
  • RDB檔案轉儲成json檔案和記憶體分析工具介紹
  • 客戶端和服務端資料結構介紹

資料庫

伺服器的資料庫

  • redis是記憶體型資料庫,所有資料都放在記憶體中
  • 儲存這些資料的是redisServer這個結構體,原始碼中該結構體包括大概300多行的程式碼。具體參考server.h/redisServer
  • 和資料庫相關的兩個屬性是:
    • int型別的dbnum:表示資料庫數量,預設16個
    • redisDb指標型別的db:資料庫物件陣列
      《redis設計與實現》2-資料庫實現篇

資料庫物件

所在檔案為server.h。資料庫中所有針對鍵值對的增刪改查,都是對dict做操作

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB  */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;
複製程式碼
  • dict:儲存了該資料庫中所有的鍵值對,鍵都是字串,值可以是多種型別
  • expires:儲存了該資料中所有設定了過期時間的key
  • blocking_keys:儲存了客戶端阻塞的
  • watched_keys:儲存被watch的命令
  • id:儲存資料庫索引
  • avg_ttl

客戶端切換資料庫

  • 客戶端通過select dbnum 命令切換選中的資料庫
  • 客戶端的資訊儲存在client這個資料結構中,參考server.h/client
  • client的型別為redisDb的db指標指向目前所選擇的資料庫

讀寫鍵空間時的其他操作

讀寫鍵空間時,是針對dict做操作,但是除了完成基本的增改查詢操作,還會執行一些額外的維護操作,包括:

  • 讀寫鍵時,會根據是否命中,更新hit和miss次數。

    相關命令:info stats keyspace_hits, info stats keyspace_misses

  • 讀取鍵後,會更新鍵的LRU時間,前面章節介紹過該欄位
  • 讀取時,如果發現鍵已經過期,會先刪除該鍵,然後才執行其他操作
  • 如果watch監視了某個鍵,修改時會標記該鍵為髒(dirty)
  • 每修改一個鍵,會對髒鍵計數器加1,觸發持久化和複製操作
  • 如果開啟通知功能,修改鍵會下發通知

設定過期時間

  • expire key ttl:設定生存時間為ttl秒
  • pexpire key ttl:設定生存時間為ttl毫秒
  • expireat key timestamp:設定過期時間為timstamp的秒數時間戳
  • pexpireat key timestamp:過期時間為毫秒時間戳
  • persist key:解除過期時間
  • ttl key:獲取剩餘生存時間

《redis設計與實現》2-資料庫實現篇

儲存過期時間

過期時間儲存在expires的字典中,值為long型別的毫秒時間戳

過期鍵刪除策略

各種刪除策略的對比

策略型別 描述 優點 缺點 redis是否採用
定時刪除 通過定時器實現 保證過期鍵能儘快釋放 對cpu不友好,影響相應時間和吞吐量
惰性刪除 放任不管,查詢時才去檢查 對cpu友好 沒有被訪問的永遠不會被釋放,相當於記憶體洩露
定期刪除 每隔一段時間檢查 綜合前面的優點 難於確定執行時長和頻率

redis使用的過期鍵刪除策略

redis採用了惰性刪除和定期刪除策略

惰性刪除的實現

  • 由db.c中的expireIfNeeded實現
  • 每次執行redis命令前都會呼叫該函式對輸入鍵做檢查

定期刪除的實現

  • server.c中的serverCron函式執行定時任務
  • 函式每次執行時,都從一定數量的資料庫中取出一定數量的鍵進行檢查,並刪除過期鍵

資料庫通知

  • 鍵空間通知:客戶端獲取資料庫中鍵執行了什麼命令。實現程式碼為notify.c檔案的notifyKeyspaceEvent函式
    subscribe __keyspace@0__:keyname
    複製程式碼
  • 鍵事件通知:某個命令被什麼鍵執行了
    subscribe __keyevent@0__:del
    複製程式碼

RDB持久化

  • redis是記憶體資料庫,為了避免伺服器程式異常導致資料丟失,redis提供了RDB持久化功能
  • 持久化後的RDB檔案是一個經過壓縮的二進位制檔案

RDB檔案的建立與載入

生成rdb檔案的兩個命令如下,實現函式為rdb.c檔案的rdbSave函式:

  • SAVE:阻塞redis伺服器程式,知道RDB建立完成。阻塞期間不能處理其他請求
  • BGSAVE:派生出子程式,子程式負責建立RDB檔案,父程式繼續處理請求

RDB檔案的載入是在伺服器啟動時自動執行的,實現函式為rdb.c檔案的rdbload函式。載入期間伺服器一直處於阻塞狀態

自動間隔儲存

redis允許使用者通過設定伺服器配置的server選項,讓伺服器每隔一段時間(100ms)自動執行BGSAVE命令(serverCron函式)

//server.c中main函式內部建立定時器,serverCron為定時任務回撥函式
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
    serverPanic("Can't create event loop timers.");
    exit(1);
}
複製程式碼

配置引數

// 任意一個配置滿足即執行
save 900 1 // 900s內,對伺服器進行至少1次修改
save 300 10 // 300s內,對伺服器至少修改10次
複製程式碼

資料結構

// 伺服器全域性變數,前面介紹過
struct redisServer {
    ...
     /* RDB persistence */
    // 上一次執行save或bgsave後,對資料庫進行了多少次修改
    long long dirty;                /* Changes to DB from the last save */
    long long dirty_before_bgsave;  /* Used to restore dirty on failed BGSAVE */
    pid_t rdb_child_pid;            /* PID of RDB saving child */
    struct saveparam *saveparams;   /* Save points array for RDB */
    int saveparamslen;              /* Number of saving points */
    char *rdb_filename;             /* Name of RDB file */
    int rdb_compression;            /* Use compression in RDB? */
    int rdb_checksum;               /* Use RDB checksum? */
    // 上一次成功執行save或bgsave的時間
    time_t lastsave;                /* Unix time of last successful save */
    time_t lastbgsave_try;          /* Unix time of last attempted bgsave */
    time_t rdb_save_time_last;      /* Time used by last RDB save run. */
    time_t rdb_save_time_start;     /* Current RDB save start time. */
    int rdb_bgsave_scheduled;       /* BGSAVE when possible if true. */
    int rdb_child_type;             /* Type of save by active child. */
    int lastbgsave_status;          /* C_OK or C_ERR */
    int stop_writes_on_bgsave_err;  /* Don't allow writes if can't BGSAVE */
    int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */
    int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */
    ...
};
// 具體每一個引數對應的變數
struct saveparam {
    time_t seconds;
    int changes;
};
複製程式碼

RDB檔案結構

概覽

《redis設計與實現》2-資料庫實現篇

  • 頭五個字元為‘redis’常量,標識這個rdb檔案是redis檔案
  • dv_version:4位元組,標識了rdb檔案的版本號
  • databases:資料庫檔案內容
  • EOF:常量,1位元組,標識檔案正文結束
  • check_sum:8位元組無符號整形,儲存校驗和,判定檔案是否有損壞

dababases部分

《redis設計與實現》2-資料庫實現篇
每個database的內容:
《redis設計與實現》2-資料庫實現篇

  • SELECTDB:常量,1位元組。標識了後面的位元組為資料庫號碼
  • db_number:資料庫號碼
  • key_value_pairs:資料庫的鍵值對,如果有過期時間,也放在一起。

key_value_pairs部分

不帶過期時間的鍵值對

type為value的型別,1位元組,代表物件型別或底層編碼,根據type決定如何讀取value

《redis設計與實現》2-資料庫實現篇

帶過期時間的鍵值對

《redis設計與實現》2-資料庫實現篇

  • EXPIRETIME:常量,1位元組,表示接下來要讀入的是一個以毫秒為單位的過期時間
  • ms:8位元組長的無符號整形,過期時間

value的編碼

每個value儲存一個值物件,與type對應。type不同,value的結構,長度也有所不同

字串物件

  • type為REDIS_RDB_TYPE_STRING, value為字串物件,而字串物件本身又包含物件的編碼和內容
  • 如果編碼為整數型別,編碼後面直接儲存整數值
    《redis設計與實現》2-資料庫實現篇
  • 如果編碼為字串型別,分為壓縮和不壓縮
    • 如果字串長度<=20位元組,不壓縮
      《redis設計與實現》2-資料庫實現篇
    • 如果字串長度>20位元組,壓縮儲存
      《redis設計與實現》2-資料庫實現篇
      • REDIS_RDB_ENC_LZF:常量,標識字串被lzf演算法壓縮過
      • compressed_len:被壓縮後的長度
      • origin_len:字串原始長度
      • compressed_string:壓縮後的內容

列表物件

  • type為REDIS_RDB_TYPE_LIST, value為列表物件
    《redis設計與實現》2-資料庫實現篇
  • list_length:記錄列表的長度
  • item:以字串物件來處理

集合物件

  • typw為REDIS_RDB_TYPE_SET,value為集合物件
    《redis設計與實現》2-資料庫實現篇
  • set_size: 集合大小
  • elem:以字串物件來處理

雜湊物件

  • type為REDIS_RDB_TYPE_HASH, value為雜湊物件
    《redis設計與實現》2-資料庫實現篇
  • hash_size:雜湊物件大小
  • key-value都以字串物件處理

有序集合物件

  • type為REDIS_RDB_TYPE_ZSET,value為有序集合物件
    《redis設計與實現》2-資料庫實現篇

intset編碼集合

  • type為REDIS_RDB_TYPE_SET_INTSET, value為整數集合物件
  • 先將結合轉換為字串物件,然後儲存。讀入時,將字串物件轉為整數集合物件

ziplist編碼的物件(包括列表,雜湊,有序集合)

  • type為REDIS_RDB_TYPE_LIST_ZIPLIST, REDIS_RDB_TYPE_HASH_ZIPLIST, REDIS_RDB_TYPE_ZSET_ZIPLIST
  • 先將壓縮列表轉換為字串物件,儲存到rdb檔案
  • 讀取時根據type型別,讀入字串,轉換為壓縮列表物件

分析RDB檔案

使用linux自帶的od命令

使用linux自帶的od命令可以檢視rdb檔案資訊,比如od -c dump.rdb,以Ascii列印,下圖顯示docker建立的redis中,空的rdb檔案輸出的內容

《redis設計與實現》2-資料庫實現篇

工具

AOF持久化

AOF寫入與同步

除了RDB持久化外,redis還提供了AOF持久化功能。區別如下:

  • RDB通過儲存資料庫中鍵值對記錄資料庫狀態
  • AOF通過儲存伺服器執行的寫命令來記錄資料庫狀態

AOF持久化分為三步:

  • 命令追加:命令append到redisServer全域性變數的aof_buf成員中
  • 檔案寫入:
  • 檔案同步

事件結束時呼叫flushAppendOnlyFile函式,考慮是否將aof_buf內容寫到AOF檔案裡(引數決定)

  • always:所有內容寫入並同步到AOF檔案(寫入的是緩衝區,同步時從緩衝區刷到磁碟)
  • everysec:預設值。寫入AOF檔案,如果上次同步時間距現在草稿1s,同步AOF。
  • no:只寫入AOF檔案,由系統決定何時同步

AOF載入與還原

伺服器只需要讀入並執行一遍AOF命令即可還原資料庫狀態,讀取的步驟如下:

  • 建立一個不帶網路連線的偽客戶端:因為命令只能在客戶端執行
  • 從AOF讀取一條寫命令
  • 使用客戶端執行該命令
  • 重複上面的步驟,直到完成

AOF重寫

  • 隨著時間流逝,AOF檔案內容會越來越大,影響redis效能。redis提供重寫功能解決該問題。
  • 重寫是通過讀取redis當前資料狀態完成的,而不是解析AOF檔案
  • 為了不影響redis正常響應,重寫功能通過建立子程式(注意不是執行緒)完成
  • 為了解決父子程式資料不一致問題(父程式接收新的請求),redis設定了AOF重寫緩衝區。新的命令在AOF緩衝區和AOF重寫緩衝區中雙寫。

事件

redis是一個事件驅動程式,事件包括兩大類:

  • 檔案事件:socket通訊完成一系列操作
  • 時間事件:某些需要在給定時間執行的操作

檔案事件

  • redis基於Reactor模式開發事件處理器,使用IO多路複用監聽套接字。關於IO多路複用可參考之前的文章五種io模型對比
  • ,雖然事件處理器以單執行緒執行,通過io多路複用,能同時監聽多個套接字實現高效能

事件處理器的構成

《redis設計與實現》2-資料庫實現篇

  • 檔案事件:套接字操作的抽象
  • io多路複用程式:同時監聽多個套接字,並向事件分派器傳送事件。多個套接字按佇列排序
  • 檔案事件分派器:接收套接字,根據事件型別呼叫相應的事件處理器
  • 事件處理器:不同的函式實現不同的事件

IO多路複用的實現

可選的io多路複用包括select,epoll,evport,kqueue實現。每種實現都放在單獨的檔案中。編譯時根據不同的巨集切換不同的實現

《redis設計與實現》2-資料庫實現篇

事件型別

#define AE_NONE 0       /* No events registered. */
#define AE_READABLE 1   /* Fire when descriptor is readable. */
#define AE_WRITABLE 2   /* Fire when descriptor is writable. */
複製程式碼

處理器

redis為檔案事件編寫了多個處理器,分別用於實現不同的網路需求,在networking.c檔案中,包括:

  • 連線應答處理器:監聽套接字,接收客戶端命令請求。對應函式為acceptTcpHandler。內部呼叫socket程式設計的accpt函式
  • 命令請求處理器:負責讀入套接字中的命令請求內容。對應函式為readQueryFromClient。內部呼叫socket程式設計的read函式
  • 命令回覆處理器:負責將回復通過套接字返回給客戶。對應函式為sendReplyToClient。內部呼叫socket班車的write函式

時間事件

分類

時間事件分類以下兩大類,取決於時間處理器的返回值:

  • 定時事件:返回AE_NOMORE(-1)
  • 週期性事件:非AE_NOMORE值。單機版只有serverCron一個週期性事件

屬性

時間事件包括三個屬性:

  • id:伺服器建立的全域性唯一標識
  • when:事件到達時間
  • timeProc:處理器,一個函式

實現

  • 所有時間事件放在一個無序連結串列中
    《redis設計與實現》2-資料庫實現篇
  • 執行時需要遍歷連結串列
  • ae.c/aeCreateTimeEvent:建立時間處理器
  • aeSearchNearestTimer:返回距離當前時間最近的時間事件
  • ae.c/processTimeEvents:遍歷時間處理器並執行

事件排程

  • 事件排程和執行由ae.c/aeProcessEvents函式負責
  • 該函式被放在ae.c/aeMain函式中的一段迴圈裡面,不斷執行直到伺服器關閉
  • aeMain被server.c的main函式呼叫
int main() {
    ...
    aeMain(server.el);
    ...
}
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

複製程式碼

客戶端

redis伺服器為每個連線的客戶端建立了一個redisClient的結構,儲存客戶端狀態資訊。所有客戶端的資訊放在一個連結串列裡。可通過client list命令檢視

struct redisServer {
    ...
    list *clients;
    ...
}
複製程式碼

客戶端資料結構如下:

typedef struct client {
    uint64_t id;            /* Client incremental unique ID. */
    //客戶端套接字描述符,偽客戶端該值為-1(包括AOF還原和執行Lua指令碼的命令)
    int fd;                 /* Client socket. */
    redisDb *db;            /* Pointer to currently SELECTed DB. */
    // 客戶端名字,預設為空,可通過client setname設定
    robj *name;             /* As set by CLIENT SETNAME. */
    // 輸入緩衝區,儲存客戶端傳送的命令請求,不能超過1G
    sds querybuf;           /* Buffer we use to accumulate client queries. */
    size_t qb_pos;          /* The position we have read in querybuf. */
    sds pending_querybuf;   /* If this client is flagged as master, this buffer
                               represents the yet not applied portion of the
                               replication stream that we are receiving from
                               the master. */
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size. */
    // 解析querybuf,得到引數個數
    int argc;               /* Num of arguments of current command. */
    // 解析querybuf,得到引數值
    robj **argv;            /* Arguments of current command. */
    // 根據前面的argv[0], 找到這個命令對應的處理函式
    struct redisCommand *cmd, *lastcmd;  /* Last command executed. */
    int reqtype;            /* Request protocol type: PROTO_REQ_* */
    int multibulklen;       /* Number of multi bulk arguments left to read. */
    long bulklen;           /* Length of bulk argument in multi bulk request. */
    // 伺服器返回給客戶端的可被空間,固定buff用完時才會使用
    list *reply;            /* List of reply objects to send to the client. */
    unsigned long long reply_bytes; /* Tot bytes of objects in reply list. */
    size_t sentlen;         /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    // 客戶端的建立時間
    time_t ctime;           /* Client creation time. */
    // 客戶端與伺服器最後一次互動的時間
    time_t lastinteraction; /* Time of the last interaction, used for timeout */
    // 客戶端空轉時間
    time_t obuf_soft_limit_reached_time;
    // 客戶端角色和狀態:REDIS_MASTER, REDIS_SLAVE, REDIS_LUA_CLIENT等
    int flags;              /* Client flags: CLIENT_* macros. */
    // 客戶端是否通過身份驗證的標識
    int authenticated;      /* When requirepass is non-NULL. */
    int replstate;          /* Replication state if this is a slave. */
    int repl_put_online_on_ack; /* Install slave write handler on ACK. */
    int repldbfd;           /* Replication DB file descriptor. */
    off_t repldboff;        /* Replication DB file offset. */
    off_t repldbsize;       /* Replication DB file size. */
    sds replpreamble;       /* Replication DB preamble. */
    long long read_reploff; /* Read replication offset if this is a master. */
    long long reploff;      /* Applied replication offset if this is a master. */
    long long repl_ack_off; /* Replication ack offset, if this is a slave. */
    long long repl_ack_time;/* Replication ack time, if this is a slave. */
    long long psync_initial_offset; /* FULLRESYNC reply offset other slaves
                                       copying this slave output buffer
                                       should use. */
    char replid[CONFIG_RUN_ID_SIZE+1]; /* Master replication ID (if master). */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    char slave_ip[NET_IP_STR_LEN]; /* Optionally given by REPLCONF ip-address */
    int slave_capa;         /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
    multiState mstate;      /* MULTI/EXEC state */
    int btype;              /* Type of blocking op if CLIENT_BLOCKED. */
    blockingState bpop;     /* blocking state */
    long long woff;         /* Last write global replication offset. */
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */
    listNode *client_list_node; /* list node in client list */

    /* Response buffer */
    // 記錄buf陣列目前使用的位元組數
    int bufpos;
    // (16*1024)=16k,伺服器返回給客戶端的內容緩衝區。固定大小,儲存一下固定返回值(如‘ok’)
    char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
複製程式碼

伺服器

伺服器記錄了redis伺服器所有的資訊,包括前面介紹的一些,羅列主要的如下:

struct redisServer {
    ...
    // 所有資料資訊
    redisDb *db;
    // 所有客戶端資訊
    list *clients;
    
     /* time cache */
    // 系統當前unix時間戳,秒
    time_t unixtime;    /* Unix time sampled every cron cycle. */
    time_t timezone;    /* Cached timezone. As set by tzset(). */
    int daylight_active;    /* Currently in daylight saving time. */
    // 系統當前unix時間戳,毫秒
    long long mstime;   /* Like 'unixtime' but with milliseconds resolution. */
    
    // 預設沒10s更新一次的時鐘快取,用於計算鍵idle時長
    unsigned int lruclock;      /* Clock for LRU eviction */
    
    // 抽樣相關的引數
    struct {
        // 上次抽樣時間
        long long last_sample_time; /* Timestamp of last sample in ms */
        // 上次抽樣時,伺服器已經執行的命令數
        long long last_sample_count;/* Count in last sample */
        // 抽樣結果
        long long samples[STATS_METRIC_SAMPLES];
        int idx;
    } inst_metric[STATS_METRIC_COUNT];
    
    // 記憶體峰值
    size_t stat_peak_memory;        /* Max used memory record */
    // 關閉伺服器的標識
    int shutdown_asap;          /* SHUTDOWN needed ASAP */
    // bgsave命令子程式的id
    pid_t rdb_child_pid;            /* PID of RDB saving child */
    // bgrewriteaof子程式id
    pid_t aof_child_pid;            /* PID if rewriting process */
    // serverCron執行次數
    int cronloops;              /* Number of times the cron function run */
    ...
}
複製程式碼

參考

相關文章