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
命令後,就會切換數據庫。我們用兩個客戶端,表示如下圖:
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可以是字符串的對象,列表對象,哈希表對象,集合對象和有序集合對象中的任意一種。
數據庫對鍵對象的刪除操作,會連帶值對象也一併刪除,因此再有一些操作中,例如RENAME等命令,中間步驟會使用刪除原有鍵,常常需要對值對象的引用計數加1,保護值對象不被刪除,當新的鍵被設置後,則對值對象的引用計數減1。
我們向一個數據庫中添加幾個鍵,並且用圖表示出來:
- 紅色代表鍵對象,有 RAW編碼的字符串對象,哈希對象。將結構簡化表示,重點關注引用計數。
- 藍色代表值對象,完成結構如圖所示。
數據庫每次根據鍵名找到值對象時,是分爲以讀操作 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 狀態被更新
// 注意:如果鍵在邏輯上已經過期但是仍然存在,函數返回NULL
robj *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內部維護一個定時任務,默認每秒運行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",key,db->id);
return dbDelete(db,key); //從數據庫中刪除key
}
3. 數據庫相關命令實現
我們只列舉部分命令實現,所有代碼註釋可以上github查看:Redis 數據庫實現(db.c)
3.1 鍵空間命令
- SCAN 一類命令的底層實現
// SCAN cursor [MATCH pattern] [COUNT count]
// SCAN、HSCAN、SSCAN、ZSCAN一類命令底層實現
// o對象必須是哈希對象或集合對象,否則命令將操作當前數據庫
// 如果o不是NULL,那麼說明他是一個哈希或集合對象,函數將跳過這些鍵對象,對參數進行分析
// 如果是哈希對象,返回返回的是鍵值對
void scanGenericCommand(client *c, robj *o, unsigned long cursor) {
int i, j;
list *keys = listCreate(); //創建一個列表
listNode *node, *nextnode;
long count = 10;
sds pat = NULL;
int patlen = 0, use_pattern = 0;
dict *ht;
/* Object must be NULL (to iterate keys names), or the type of the object
* must be Set, Sorted Set, or Hash. */
// 輸入類型的檢查,要麼迭代鍵名,要麼當前集合對象,要麼迭代哈希對象,要麼迭代有序集合對象
serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH ||
o->type == OBJ_ZSET);
/* Set i to the first option argument. The previous one is the cursor. */
// 計算第一個參數的下標,如果是鍵名,要條跳過該鍵
i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */
/* Step 1: Parse options. */
// 1. 解析選項
while (i < c->argc) {
j = c->argc - i;
// 設定COUNT參數,COUNT 選項的作用就是讓用戶告知迭代命令, 在每次迭代中應該返回多少元素。
if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
//保存個數到count
if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL)
!= C_OK)
{
goto cleanup;
}
// 如果個數小於1,語法錯誤
if (count < 1) {
addReply(c,shared.syntaxerr);
goto cleanup;
}
i += 2; //參數跳過兩個已經解析過的
// 設定MATCH參數,讓命令只返回和給定模式相匹配的元素。
} else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
pat = c->argv[i+1]->ptr; //pattern字符串
patlen = sdslen(pat); //pattern字符串長度
/* The pattern always matches if it is exactly "*", so it is
* equivalent to disabling it. */
// 如果pattern是"*",就不用匹配,全部返回,設置爲0
use_pattern = !(pat[0] == '*' && patlen == 1);
i += 2;
} else {
addReply(c,shared.syntaxerr);
goto cleanup;
}
}
/* Step 2: Iterate the collection.
*
* Note that if the object is encoded with a ziplist, intset, or any other
* representation that is not a hash table, we are sure that it is also
* composed of a small number of elements. So to avoid taking state we
* just return everything inside the object in a single call, setting the
* cursor to zero to signal the end of the iteration. */
/* Handle the case of a hash table. */
// 2.如果對象是ziplist、intset或其他而不是哈希表,那麼這些類型只是包含少量的元素
// 我們一次將其所有的元素全部返回給調用者,並設置遊標cursor爲0,標示迭代完成
ht = NULL;
// 迭代目標是數據庫
if (o == NULL) {
ht = c->db->dict;
// 迭代目標是HT編碼的集合對象
} else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
ht = o->ptr;
// 迭代目標是HT編碼的哈希對象
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
ht = o->ptr;
count *= 2; /* We return key / value for this type. */
// 迭代目標是skiplist編碼的有序集合對象
} else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
ht = zs->dict;
count *= 2; /* We return key / value for this type. */
}
if (ht) {
void *privdata[2];
/* We set the max number of iterations to ten times the specified
* COUNT, so if the hash table is in a pathological state (very
* sparsely populated) we avoid to block too much time at the cost
* of returning no or very few elements. */
// 設置最大的迭代長度爲10*count次
long maxiterations = count*10;
/* We pass two pointers to the callback: the list to which it will
* add new elements, and the object containing the dictionary so that
* it is possible to fetch more data in a type-dependent way. */
// 回調函數scanCallback的參數privdata是一個數組,保存的是被迭代對象的鍵和值
// 回調函數scanCallback的另一個參數,是一個字典對象
// 回調函數scanCallback的作用,從字典對象中將鍵值對提取出來,不用管字典對象是什麼數據類型
privdata[0] = keys;
privdata[1] = o;
// 循環掃描ht,從遊標cursor開始,調用指定的scanCallback函數,提出ht中的數據到剛開始創建的列表keys中
do {
cursor = dictScan(ht, cursor, scanCallback, privdata);
} while (cursor &&
maxiterations-- &&
listLength(keys) < (unsigned long)count);//沒迭代完,或沒迭代夠count,就繼續循環
// 如果是集合對象但編碼不是HT是整數集合
} else if (o->type == OBJ_SET) {
int pos = 0;
int64_t ll;
// 將整數值取出來,構建成字符串對象加入到keys列表中,遊標設置爲0,表示迭代完成
while(intsetGet(o->ptr,pos++,&ll))
listAddNodeTail(keys,createStringObjectFromLongLong(ll));
cursor = 0;
// 如果是哈希對象,或有序集合對象,但是編碼都不是HT,是ziplist
} else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) {
unsigned char *p = ziplistIndex(o->ptr,0);
unsigned char *vstr;
unsigned int vlen;
long long vll;
while(p) {
// 將值取出來,根據不同類型的值,構建成相同的字符串對象,加入到keys列表中
ziplistGet(p,&vstr,&vlen,&vll);
listAddNodeTail(keys,
(vstr != NULL) ? createStringObject((char*)vstr,vlen) :
createStringObjectFromLongLong(vll));
p = ziplistNext(o->ptr,p);
}
cursor = 0;
} else {
serverPanic("Not handled encoding in SCAN.");
}
/* Step 3: Filter elements. */
// 3. 如果設置MATCH參數,要進行過濾
node = listFirst(keys); //鏈表首節點地址
while (node) {
robj *kobj = listNodeValue(node); //key對象
nextnode = listNextNode(node); //下一個節點地址
int filter = 0; //默認爲不過濾
/* Filter element if it does not match the pattern. */
//pattern不是"*"因此要過濾
if (!filter && use_pattern) {
// 如果kobj是字符串對象
if (sdsEncodedObject(kobj)) {
// kobj的值不匹配pattern,設置過濾標誌
if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0))
filter = 1;
// 如果kobj是整數對象
} else {
char buf[LONG_STR_SIZE];
int len;
serverAssert(kobj->encoding == OBJ_ENCODING_INT);
// 將整數轉換爲字符串類型,保存到buf中
len = ll2string(buf,sizeof(buf),(long)kobj->ptr);
//buf的值不匹配pattern,設置過濾標誌
if (!stringmatchlen(pat, patlen, buf, len, 0)) filter = 1;
}
}
/* Filter element if it is an expired key. */
// 迭代目標是數據庫,如果kobj是過期鍵,則過濾
if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;
/* Remove the element and its associted value if needed. */
// 如果該鍵滿足了上述的過濾條件,那麼將其從keys列表刪除並釋放
if (filter) {
decrRefCount(kobj);
listDelNode(keys, node);
}
/* If this is a hash or a sorted set, we have a flat list of
* key-value elements, so if this element was filtered, remove the
* value, or skip it if it was not filtered: we only match keys. */
// 如果當前迭代目標是有序集合或哈希對象,因此keys列表中保存的是鍵值對,如果key鍵對象被過濾,值對象也應當被過濾
if (o && (o->type == OBJ_ZSET || o->type == OBJ_HASH)) {
node = nextnode;
nextnode = listNextNode(node); //值對象的節點地址
// 如果該鍵滿足了上述的過濾條件,那麼將其從keys列表刪除並釋放
if (filter) {
kobj = listNodeValue(node); //取出值對象
decrRefCount(kobj);
listDelNode(keys, node); //刪除
}
}
node = nextnode;
}
/* Step 4: Reply to the client. */
// 4. 回覆信息給client
addReplyMultiBulkLen(c, 2); //2部分,一個是遊標,一個是列表
addReplyBulkLongLong(c,cursor); //回覆遊標
addReplyMultiBulkLen(c, listLength(keys)); //回覆列表長度
//循環回覆列表中的元素,並釋放
while ((node = listFirst(keys)) != NULL) {
robj *kobj = listNodeValue(node);
addReplyBulk(c, kobj);
decrRefCount(kobj);
listDelNode(keys, node);
}
// 清理代碼
cleanup:
listSetFreeMethod(keys,decrRefCountVoid); //設置特定的釋放列表的方式decrRefCountVoid
listRelease(keys); //釋放
}
- RENAME、RENAMENX命令底層實現
// RENAME key newkey
// RENAMENX key newkey
// RENAME、RENAMENX命令底層實現
void renameGenericCommand(client *c, int nx) {
robj *o;
long long expire;
int samekey = 0;
/* When source and dest key is the same, no operation is performed,
* if the key exists, however we still return an error on unexisting key. */
// key和newkey相同的話,設置samekey標誌
if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) samekey = 1;
// 以寫操作讀取key的值對象
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
return;
// 如果key和newkey相同,nx爲1發送0,否則爲ok
if (samekey) {
addReply(c,nx ? shared.czero : shared.ok);
return;
}
// 增加值對象的引用計數,保護起來,用於關聯newkey,以防刪除了key順帶將值對象也刪除
incrRefCount(o);
// 備份key的過期時間,將來作爲newkey的過期時間
expire = getExpire(c->db,c->argv[1]);
// 判斷newkey的值對象是否存在
if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
// 設置nx標誌,則不符合已存在的條件,發送0
if (nx) {
decrRefCount(o);
addReply(c,shared.czero);
return;
}
/* Overwrite: delete the old key before creating the new one
* with the same name. */
dbDelete(c->db,c->argv[2]); //將舊的newkey對象刪除
}
// 將newkey和key的值對象關聯
dbAdd(c->db,c->argv[2],o);
// 如果newkey設置過過期時間,則爲newkey設置過期時間
if (expire != -1) setExpire(c->db,c->argv[2],expire);
// 刪除key
dbDelete(c->db,c->argv[1]);
// 發送這兩個鍵被修改的信號
signalModifiedKey(c->db,c->argv[1]);
signalModifiedKey(c->db,c->argv[2]);
// 發送不同命令的事件通知
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from",
c->argv[1],c->db->id);
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_to",
c->argv[2],c->db->id);
server.dirty++; //更新髒鍵
addReply(c,nx ? shared.cone : shared.ok);
}
- MOVE 命令
// MOVE key db 將當前數據庫的 key 移動到給定的數據庫 db 當中。
// MOVE 命令實現
void moveCommand(client *c) {
robj *o;
redisDb *src, *dst;
int srcid;
long long dbid, expire;
// 服務器處於集羣模式,不支持多數據庫
if (server.cluster_enabled) {
addReplyError(c,"MOVE is not allowed in cluster mode");
return;
}
/* Obtain source and target DB pointers */
// 獲得源數據庫和源數據庫的id
src = c->db;
srcid = c->db->id;
// 將參數db的值保存到dbid,並且切換到該數據庫中
if (getLongLongFromObject(c->argv[2],&dbid) == C_ERR ||
dbid < INT_MIN || dbid > INT_MAX ||
selectDb(c,dbid) == C_ERR)
{
addReply(c,shared.outofrangeerr);
return;
}
// 目標數據庫
dst = c->db;
// 切換回源數據庫
selectDb(c,srcid); /* Back to the source DB */
/* If the user is moving using as target the same
* DB as the source DB it is probably an error. */
// 如果前後切換的數據庫相同,則返回有關錯誤
if (src == dst) {
addReply(c,shared.sameobjecterr);
return;
}
/* Check if the element exists and get a reference */
// 以寫操作取出源數據庫的對象
o = lookupKeyWrite(c->db,c->argv[1]);
if (!o) {
addReply(c,shared.czero); //不存在發送0
return;
}
// 備份key的過期時間
expire = getExpire(c->db,c->argv[1]);
/* Return zero if the key already exists in the target DB */
// 判斷當前key是否存在於目標數據庫,存在直接返回,發送0
if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
addReply(c,shared.czero);
return;
}
// 將key-value對象添加到目標數據庫中
dbAdd(dst,c->argv[1],o);
// 設置移動後key的過期時間
if (expire != -1) setExpire(dst,c->argv[1],expire);
incrRefCount(o); //增加引用計數
/* OK! key moved, free the entry in the source DB */
// 從源數據庫中將key和關聯的值對象刪除
dbDelete(src,c->argv[1]);
server.dirty++; //更新髒鍵
addReply(c,shared.cone); //回覆1
}
3.2 過期命令
- EXPIRE, PEXPIRE, EXPIREAT,PEXPIREAT命令的底層實現
// EXPIRE key seconds
// EXPIREAT key timestamp
// PEXPIRE key milliseconds
// PEXPIREAT key milliseconds-timestamp
// EXPIRE, PEXPIRE, EXPIREAT,PEXPIREAT命令的底層實現
// basetime參數可能是絕對值,可能是相對值。執行AT命令時basetime爲0,否則保存的是當前的絕對時間
// unit 是UNIT_SECONDS 或者 UNIT_MILLISECONDS,但是basetime總是以毫秒爲單位的。
void expireGenericCommand(client *c, long long basetime, int unit) {
robj *key = c->argv[1], *param = c->argv[2];
long long when; /* unix time in milliseconds when the key will expire. */
// 取出時間參數保存到when中
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
return;
// 如果過期時間是以秒爲單位,則轉換爲毫秒值
if (unit == UNIT_SECONDS) when *= 1000;
// 絕對時間
when += basetime;
/* No key, return zero. */
// 判斷key是否在數據庫中,不在返回0
if (lookupKeyWrite(c->db,key) == NULL) {
addReply(c,shared.czero);
return;
}
/* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
* should never be executed as a DEL when load the AOF or in the context
* of a slave instance.
*
* Instead we take the other branch of the IF statement setting an expire
* (possibly in the past) and wait for an explicit DEL from the master. */
// 如果當前正在載入AOF數據或者在從節點環境中,即使EXPIRE的TTL爲負數,或者EXPIREAT的時間戳已經過期
// 服務器都不會執行DEL命令,且將過期TTL設置爲鍵的過期時間,等待主節點發來的DEL命令
// 如果when已經過時,服務器爲主節點且沒有載入AOF數據
if (when <= mstime() && !server.loading && !server.masterhost) {
robj *aux;
// 將key從數據庫中刪除
serverAssertWithInfo(c,key,dbDelete(c->db,key));
server.dirty++; //更新髒鍵
/* Replicate/AOF this as an explicit DEL. */
// 創建一個"DEL"命令
aux = createStringObject("DEL",3);
rewriteClientCommandVector(c,2,aux,key); //修改客戶端的參數列表爲DEL命令
decrRefCount(aux);
// 發送鍵被修改的信號
signalModifiedKey(c->db,key);
// 發送"del"的事件通知
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c, shared.cone);
return;
// 如果當前服務器是從節點,或者服務器正在載入AOF數據
// 不管when有沒有過時,都設置爲過期時間
} else {
// 設置過期時間
setExpire(c->db,key,when);
addReply(c,shared.cone);
signalModifiedKey(c->db,key); //發送鍵被修改的信號
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id); //發送"expire"的事件通知
server.dirty++; //更新髒鍵
return;
}
}
- TTL、PTTL 命令底層實現
// TTL key
// PTTL key
// TTL、PTTL命令底層實現,output_ms爲1,返回毫秒,爲0返回秒
void ttlGenericCommand(client *c, int output_ms) {
long long expire, ttl = -1;
/* If the key does not exist at all, return -2 */
// 判斷key是否存在於數據庫,並且不修改鍵的使用時間
if (lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH) == NULL) {
addReplyLongLong(c,-2);
return;
}
/* The key exists. Return -1 if it has no expire, or the actual
* TTL value otherwise. */
// 如果key存在,則備份當前key的過期時間
expire = getExpire(c->db,c->argv[1]);
// 如果設置了過期時間
if (expire != -1) {
ttl = expire-mstime(); //計算生存時間
if (ttl < 0) ttl = 0;
}
// 如果鍵是永久的
if (ttl == -1) {
addReplyLongLong(c,-1); //發送-1
} else {
addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000)); //發送生存時間
}
}