天天看點

為什麼redis取出來是null_Redis源碼解析十四--Redis 資料庫及相關指令實作(db)Redis 資料庫及相關指令實作

Redis 資料庫及相關指令實作

1. 資料庫管理指令

資料庫管理的指令如下表格所示:redis keys指令詳解

FLUSHDB清空目前資料庫的所有key

FLUSHALL清空整個Redis伺服器的所有key

DBSIZE傳回目前資料庫的key的個數

DEL key [key …]删除一個或多個鍵

EXISTS key檢查給定key是否存在

SELECT id切換到指定的資料庫

RANDOMKEY從目前資料庫中随機傳回(不删除)一個 key 。

KEYS pattern查找所有符合給定模式pattern的key

SCAN cursor [MATCH pattern] [COUNT count]增量式疊代目前資料庫鍵

LASTSAVE傳回最近一次成功将資料儲存到磁盤上的時間,以 UNIX 時間戳格式表示。

TYPE key傳回指定鍵的對象類型

SHUTDOWN停止所有用戶端,關閉 redis 伺服器(server)

RENAME key newkey重命名指定的key,newkey存在時覆寫

RENAMENX key newkey重命名指定的key,當且僅當newkey不存在時操作

MOVE key db移動key到指定資料庫

EXPIREAT key timestamp為 key 設定生存時間,EXPIREAT 指令接受的時間參數是 UNIX 時間戳

EXPIRE key seconds以秒為機關設定 key 的生存時間

PEXPIRE key milliseconds以毫秒為機關設定 key 的生存時間

PEXPIREAT key milliseconds-timestamp以毫秒為機關設定 key 的過期 unix 時間戳

TTL key以秒為機關傳回 key 的剩餘生存時間

PTTL key以毫秒為機關傳回 key 的剩餘生存時間

2. 資料庫的實作

2.1資料庫的結構

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) */ // 儲存着 處于阻塞狀态的鍵,value為NULL dict *ready_keys; /* Blocked keys that received a PUSH */ // 事物子產品,用于儲存被WATCH指令所監控的鍵 dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ // 當記憶體不足時,Redis會根據LRU算法回收一部分鍵所占的空間,而該eviction_pool是一個長為16數組,儲存可能被回收的鍵 // eviction_pool中所有鍵按照idle空轉時間,從小到大排序,每次回收空轉時間最長的鍵 struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */ // 資料庫ID int id; /* Database ID */ // 鍵的平均過期時間 long long avg_ttl; /* Average TTL, just for stats */} redisDb;
           

blocking_keys 和 ready_keys 使用于在清單類型的阻塞指令(BLPOP等),詳細内容看:Redis 清單鍵指令實作

watched_keys 是用于事物子產品。

eviction_pool 是Redis在記憶體不足情況下,要回收記憶體時所使用。

dict 和 expires 和 id是本篇主要讨論的。

Redis伺服器和用戶端也都儲存有資料庫的資訊,下面截取出來:

typedef struct client { redisDb *db; /* Pointer to currently SELECTed DB. */} client;struct redisServer { redisDb *db; int dbnum; /* Total number of configured DBs */};
           

Redis伺服器在初始化時,會建立一個長度為dbnum(預設為16)個 redisDb類型數組,用戶端登入時,預設的資料庫為0号資料庫。當執行SELECT index指令後,就會切換資料庫。我們用兩個用戶端,表示如下圖:

為什麼redis取出來是null_Redis源碼解析十四--Redis 資料庫及相關指令實作(db)Redis 資料庫及相關指令實作

SELECT index指令非常簡單,源碼如下:

// 切換資料庫int selectDb(client *c, int id) { // id非法,傳回錯誤 if (id < 0 || id >= server.dbnum) return C_ERR; // 設定目前client的資料庫 c->db = &server.db[id]; return C_OK;}
           

2.2 資料庫的鍵值對字典

Redis是一個key-value資料庫伺服器,它将所有的鍵值對都儲存在 redisDb 結構中的 dict 字典成員中(Redis 字典結構源碼剖析)。

鍵值對字典的鍵,就是資料庫的key,每一個key都是字元串的對象。

鍵值對字典的值,就是資料庫的value,每一個value可以是字元串的對象,清單對象,哈希表對象,集合對象和有序集合對象中的任意一種。

Redis 對象系統源碼剖析

資料庫對鍵對象的删除操作,會連帶值對象也一并删除,是以再有一些操作中,例如RENAME等指令,中間步驟會使用删除原有鍵,常常需要對值對象的引用計數加1,保護值對象不被删除,當新的鍵被設定後,則對值對象的引用計數減1。

我們向一個資料庫中添加幾個鍵,并且用圖表示出來:

紅色代表鍵對象,有 RAW編碼的字元串對象,哈希對象。将結構簡化表示,重點關注引用計數。

藍色代表值對象,完成結構如圖所示。

為什麼redis取出來是null_Redis源碼解析十四--Redis 資料庫及相關指令實作(db)Redis 資料庫及相關指令實作

資料庫每次根據鍵名找到值對象時,是分為以讀操作 lookupKeyRead() 或寫操作 lookupKeyWrite() 的方式取出的,而這兩種有一定的差別,下面展示源碼:

lookupKey()函數

讀操作 lookupKeyRead() 或寫操作 lookupKeyWrite()都會調用這個底層的函數,這個函數非常簡單,就是從鍵值對字典中先找到鍵名對應的鍵對象,然後取出值對象

// 該函數被lookupKeyRead()和lookupKeyWrite()和lookupKeyReadWithFlags()調用// 從資料庫db中取出key的值對象,如果存在傳回該對象,否則傳回NULL// 傳回key對象的值對象robj *lookupKey(redisDb *db, robj *key, int flags) { // 在資料庫中查找key對象,傳回儲存該key的節點位址 dictEntry *de = dictFind(db->dict,key->ptr); if (de) { //如果找到 robj *val = dictGetVal(de); //取出鍵對應的值對象 /* Update the access time for the ageing algorithm. * Don't do it if we have a saving child, as this will trigger * a copy on write madness. */ // 更新鍵的使用時間 if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && !(flags & LOOKUP_NOTOUCH)) { val->lru = LRU_CLOCK(); } return val; //傳回值對象 } else { return NULL; }
           

lookupKeyRead()函數

lookupKeyRead()函數調用了lookupKeyReadWithFlags()函數,後者其實就判斷了一下目前鍵是否過期,如果沒有過期,更新 misses 和 hits 資訊,然後就傳回值對象。

還有就是兩個宏:

define LOOKUP_NONE 0 //zero,沒有特殊意義

define LOOKUP_NOTOUCH (1<<0) //不修改鍵的使用時間,如果隻是想判斷key的值對象的編碼類型(TYPE指令)我們不希望改變鍵的使用時間。

// 以讀操作取出key的值對象,會更新是否命中的資訊robj *lookupKeyRead(redisDb *db, robj *key) { return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);}// 以讀操作取出key的值對象,沒找到傳回NULL// 調用該函數的副作用如下:// 1.如果一個鍵的到達過期時間TTL,該鍵被設定為過期的// 2.鍵的使用時間資訊被更新// 3.全局鍵 hits/misses 狀态被更新// 注意:如果鍵在邏輯上已經過期但是仍然存在,函數傳回NULLrobj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { robj *val; // 如果鍵已經過期且被删除 if (expireIfNeeded(db,key) == 1) { /* Key expired. If we are in the context of a master, expireIfNeeded() * returns 0 only when the key does not exist at all, so it's save * to return NULL ASAP. */ // 鍵已過期,如果是主節點環境,表示key已經絕對被删除,如果是從節點, if (server.masterhost == NULL) return NULL; // 如果我們在從節點環境, expireIfNeeded()函數不會删除過期的鍵,它傳回的僅僅是鍵是否被删除的邏輯值 // 過期的鍵由主節點負責,為了保證主從節點資料的一緻 if (server.current_client && server.current_client != server.master && server.current_client->cmd && server.current_client->cmd->flags & CMD_READONLY) { return NULL; } } // 鍵沒有過期,則傳回鍵的值對象 val = lookupKey(db,key,flags); // 更新 是否命中 的資訊 if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val;}
           
  • lookupKeyWrite()函數

lookupKeyWrite() 函數則先判斷鍵是否過期,然後直接調用最底層的 lookupKey() 函數,和 lookupKeyRead()函數 相比,少了一步更新 misses 和 hits 資訊的過程。

// 以寫操作取出key的值對象,不更新是否命中的資訊robj *lookupKeyWrite(redisDb *db, robj *key) { expireIfNeeded(db,key); return lookupKey(db,key,LOOKUP_NONE);}
           

2.3 鍵的過期時間

redisBb結構中的 expires 字典儲存這設定了過期時間的鍵和過期的時間。通過 EXPIRE 、 PEXPIRE、 EXPIREAT 和 PEXPIREAT四個指令,用戶端可以給某個存在的鍵設定過期時間,當鍵的過期時間到達時,鍵就不再可用。

我們先用圖展示一下資料庫中的過期字典,用剛才的鍵值對字典中的對象。

為什麼redis取出來是null_Redis源碼解析十四--Redis 資料庫及相關指令實作(db)Redis 資料庫及相關指令實作

很明顯,鍵值對字典和過期字典中的相同對象隻占一份空間,隻是增加引用計數。

我們重點讨論過期鍵的删除政策:

惰性删除:當客戶度讀出帶有逾時屬性的鍵時,如果已經超過鍵設定的過期時間,會執行删除并傳回空。

定時删除:Redis内部維護一個定時任務,預設每秒運作10次。

我們給出惰性删除的代碼,這個函數 expireIfNeeded(),所有讀寫資料庫的Redis指令在執行前都會調用,删除過期鍵。

// 檢查鍵是否過期,如果過期,從資料庫中删除// 傳回0表示沒有過期或沒有過期時間,傳回1 表示鍵被删除int expireIfNeeded(redisDb *db, robj *key) { //得到過期時間,機關毫秒 mstime_t when = getExpire(db,key); mstime_t now; // 沒有過期時間,直接傳回 if (when < 0) return 0; /* No expire for this key */ /* Don't expire anything while loading. It will be done later. */ // 伺服器正在載入,那麼不進行過期檢查 if (server.loading) return 0; /* If we are in the context of a Lua script, we claim that time is * blocked to when the Lua script started. This way a key can expire * only the first time it is accessed and not in the middle of the * script execution, making propagation to slaves / AOF consistent. * See issue #1525 on Github for more information. */ // 傳回一個Unix時間,機關毫秒 now = server.lua_caller ? server.lua_time_start : mstime(); /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ // 如果伺服器正在進行主從節點的複制,從節點的過期鍵應該被 主節點發送同步删除的操作 删除,而自己不主動删除 // 從節點隻傳回正确的邏輯資訊,0表示key仍然沒有過期,1表示key過期。 if (server.masterhost != NULL) return now > when; /* Return when this key has not expired */ // 當鍵還沒有過期時,直接傳回0 if (now <= when) return 0; /* Delete the key */ // 鍵已經過期,删除鍵 server.stat_expiredkeys++; //過期鍵的數量加1 propagateExpire(db,key); //将過期鍵key傳播給AOF檔案和從節點 notifyKeyspaceEvent(NOTIFY_EXPIRED, //發送"expired"事件通知 "expired