《redis設計與實現》2-數據庫實現篇

上一篇文章介紹了redis基本的數據結構和對象《redis設計與實現》1-數據結構與對象篇node

本文主要關於:linux

  • redis數據庫實現的介紹
  • 前面介紹的各類數據,在redis服務器中的內存模型是什麼樣的的。
  • RDB文件將這些內存數據持久化後的格式是什麼樣的
  • RDB和AOF序列化的區別是什麼
  • redis提供什麼機制保障AOF文件不會一直增加
  • RDB文件轉儲成json文件和內存分析工具介紹
  • 客戶端和服務端數據結構介紹

數據庫

服務器的數據庫

  • redis是內存型數據庫,全部數據都放在內存中
  • 保存這些數據的是redisServer這個結構體,源碼中該結構體包括大概300多行的代碼。具體參考server.h/redisServer
  • 和數據庫相關的兩個屬性是:
    • int類型的dbnum:表示數據庫數量,默認16個
    • redisDb指針類型的db:數據庫對象數組

數據庫對象

所在文件爲server.h。數據庫中全部針對鍵值對的增刪改查,都是對dict作操做git

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作操做,可是除了完成基本的增改查找操做,還會執行一些額外的維護操做,包括:github

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

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

  • 讀取鍵後,會更新鍵的LRU時間,前面章節介紹過該字段
  • 讀取時,若是發現鍵已通過期,會先刪除該鍵,而後才執行其餘操做
  • 若是watch監視了某個鍵,修改時會標記該鍵爲髒(dirty)
  • 每修改一個鍵,會對髒鍵計數器加1,觸發持久化和複製操做
  • 若是開啓通知功能,修改鍵會下發通知

設置過時時間

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

保存過時時間

過時時間保存在expires的字典中,值爲long類型的毫秒時間戳算法

過時鍵刪除策略

各類刪除策略的對比

策略類型 描述 優勢 缺點 redis是否採用
定時刪除 經過定時器實現 保證過時鍵能儘快釋放 對cpu不友好,影響相應時間和吞吐量
惰性刪除 聽任無論,查詢時纔去檢查 對cpu友好 沒有被訪問的永遠不會被釋放,至關於內存泄露
按期刪除 每隔一段時間檢查 綜合前面的優勢 難於肯定執行時長和頻率

redis使用的過時鍵刪除策略

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

惰性刪除的實現

  • 由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函數)json

//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’常量,標識這個rdb文件是redis文件
  • dv_version:4字節,標識了rdb文件的版本號
  • databases:數據庫文件內容
  • EOF:常量,1字節,標識文件正文結束
  • check_sum:8字節無符號整形,保存校驗和,斷定文件是否有損壞

dababases部分

每一個database的內容:

  • SELECTDB:常量,1字節。標識了後面的字節爲數據庫號碼
  • db_number:數據庫號碼
  • key_value_pairs:數據庫的鍵值對,若是有過時時間,也放在一塊兒。

key_value_pairs部分

不帶過時時間的鍵值對

type爲value的類型,1字節,表明對象類型或底層編碼,根據type決定如何讀取value

帶過時時間的鍵值對

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

value的編碼

每一個value保存一個值對象,與type對應。type不一樣,value的結構,長度也有所不一樣

字符串對象

  • type爲REDIS_RDB_TYPE_STRING, value爲字符串對象,而字符串對象自己又包含對象的編碼和內容
  • 若是編碼爲整數類型,編碼後面直接保存整數值
  • 若是編碼爲字符串類型,分爲壓縮和不壓縮
    • 若是字符串長度<=20字節,不壓縮
    • 若是字符串長度>20字節,壓縮保存
      • REDIS_RDB_ENC_LZF:常量,標識字符串被lzf算法壓縮過
      • compressed_len:被壓縮後的長度
      • origin_len:字符串原始長度
      • compressed_string:壓縮後的內容

列表對象

  • type爲REDIS_RDB_TYPE_LIST, value爲列表對象
  • list_length:記錄列表的長度
  • item:以字符串對象來處理

集合對象

  • typw爲REDIS_RDB_TYPE_SET,value爲集合對象
  • set_size: 集合大小
  • elem:以字符串對象來處理

哈希對象

  • type爲REDIS_RDB_TYPE_HASH, value爲哈希對象
  • hash_size:哈希對象大小
  • key-value都以字符串對象處理

有序集合對象

  • type爲REDIS_RDB_TYPE_ZSET,value爲有序集合對象

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文件輸出的內容

工具

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多路複用,能同時監聽多個套接字實現高性能

事件處理器的構成

  • 文件事件:套接字操做的抽象
  • io多路複用程序:同時監聽多個套接字,並向事件分派器傳送事件。多個套接字按隊列排序
  • 文件事件分派器:接收套接字,根據事件類型調用相應的事件處理器
  • 事件處理器:不一樣的函數實現不一樣的事件

IO多路複用的實現

可選的io多路複用包括select,epoll,evport,kqueue實現。每種實現都放在單獨的文件中。編譯時根據不一樣的宏切換不一樣的實現

事件類型

#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:處理器,一個函數

實現

  • 全部時間事件放在一個無序鏈表中
  • 執行時須要遍歷鏈表
  • 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 */
    ...
}
複製代碼

參考

相關文章
相關標籤/搜索