【Redis5源碼學習】淺析redis命令之move篇

Graperedis


命令語法

命令含義:將當前數據庫的 key 移動到給定的數據庫 db 當中。
命令註釋:若是當前數據庫(源數據庫)和給定數據庫(目標數據庫)有相同名字的給定 key ,或者 key 不存在於當前數據庫,那麼 MOVE 沒有任何效果。所以,也能夠利用這一特性,將 MOVE 看成鎖(locking)原語(primitive)。
命令格式:數據庫

MOVE key db

命令實戰:數組

# key 存在於當前數據庫
    redis> SELECT 0                             # redis默認使用數據庫 0,爲了清晰起見,這裏再顯式指定一次。
    OK
    redis> SET song "secret base - Zone"
    OK
    redis> MOVE song 1                          # 將 song 移動到數據庫 1
    (integer) 1
    redis> EXISTS song                          # song 已經被移走
    (integer) 0
    redis> SELECT 1                             # 使用數據庫 1
    OK
    redis:1> EXISTS song                        # 證明 song 被移到了數據庫 1 (注意命令提示符變成了"redis:1",代表正在使用數據庫 1)
    (integer) 1
    
    # 當 key 不存在的時候
    redis:1> EXISTS fake_key
    (integer) 0
    redis:1> MOVE fake_key 0                    # 試圖從數據庫 1 移動一個不存在的 key 到數據庫 0,失敗
    (integer) 0
    redis:1> select 0                           # 使用數據庫0
    OK
    redis> EXISTS fake_key                      # 證明 fake_key 不存在
    (integer) 0
    
    # 當源數據庫和目標數據庫有相同的 key 時
    redis> SELECT 0                             # 使用數據庫0
    OK
    redis> SET favorite_fruit "banana"
    OK
    redis> SELECT 1                             # 使用數據庫1
    OK
    redis:1> SET favorite_fruit "apple"
    OK
    redis:1> SELECT 0                           # 使用數據庫0,並試圖將 favorite_fruit 移動到數據庫 1
    OK
    redis> MOVE favorite_fruit 1                # 由於兩個數據庫有相同的 key,MOVE 失敗
    (integer) 0
    redis> GET favorite_fruit                   # 數據庫 0 的 favorite_fruit 沒變
    "banana"
    redis> SELECT 1
    OK
    redis:1> GET favorite_fruit                 # 數據庫 1 的 favorite_fruit 也是
    "apple"

返回值
移動成功返回 1 ,失敗則返回 0 。app

源碼分析

moveCommand函數,這個是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;
    }
   
    //從客戶端信息中獲取當前db信息 
    src = c->db;
    srcid = c->db->id;
    //c->argv是參數數組,argv[1]存儲的是移動的key,argv[2]存儲的是目標數據庫
    //getLongLongFromObject獲取目標數據庫id,強轉爲int類型
    //判斷條件所以爲強轉字符串爲int,判斷是否在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 (src == dst) {
        addReply(c,shared.sameobjecterr);
        return;
    }
    /* 檢查這個key是否存在原數據庫並其信息*/
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (!o) {
        addReply(c,shared.czero);
        return;
    }
    //獲取這個key的過時時間,沒有則返回-1
    expire = getExpire(c->db,c->argv[1]);
    //查詢這個key在目標數據庫是否存在,不存在則返回錯誤信息
    if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
        addReply(c,shared.czero);
        return;
    }
    //把這個key以及這個對象加入到目標數據庫
    dbAdd(dst,c->argv[1],o);
    if (expire != -1) setExpire(c,dst,c->argv[1],expire);
    incrRefCount(o);
    /*移動完成,刪除原數據庫 */
    dbDelete(src,c->argv[1]);
    server.dirty++;
    addReply(c,shared.cone);
}

dbAdd函數:在move命令中咱們要向目標數據庫中添加key,這個命令就是關鍵。源碼分析

void dbAdd(redisDb *db, robj *key, robj *val) {
    //複製key
    sds copy = sdsdup(key->ptr);
    //把這個key插入到dict中,copy中是key,val是key對應的值
    int retval = dictAdd(db->dict, copy, val);
    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    if (val->type == OBJ_LIST ||
        val->type == OBJ_ZSET)
        signalKeyAsReady(db, key);
    if (server.cluster_enabled) slotToKeyAdd(key);
}

dictAdd函數:dbAdd中調用此函數,向dict增長entry。ui

int dictAdd(dict *d, void *key, void *val)
{
    //向dict插入一個key,返回entry
    dictEntry *entry = dictAddRaw(d,key,NULL);
    if (!entry) return DICT_ERR;
    //設置這個entry的值
    dictSetVal(d, entry, val);
    return DICT_OK;
}

GDB過程

首先設置key爲kkkk的值爲2,而後執行move命令code

127.0.0.1:6380> set kkkk 2
OK
127.0.0.1:6380> select 0
OK
127.0.0.1:6380> move kkkk 1

1.咱們先打印客戶端傳入的參數,能夠看到,argv的三個元素依次爲 move,kkkk,1:server

(gdb) p (char*)c->argv[0].ptr
$10 = 0x7f175b820ae3 "move"
(gdb) p (char*)c->argv[1].ptr
$11 = 0x7f175b820afb "kkkk"
(gdb) p (char*)c->argv[2].ptr
$12 = 0x7f175b820acb "1"

2.接着咱們來到getLongLongFromObject這個函數,在上文咱們說過了這個函數的做用是把數據強轉爲int型。在以前的文章中已經作過講述,此處再也不贅述。而後走到第二個判斷條件判斷dbid的範圍,最後是切換到目標數據庫,符合上文推理:對象

(gdb) n
934        if (getLongLongFromObject(c->argv[2],&dbid) == C_ERR ||
(gdb) n
935            dbid < INT_MIN || dbid > INT_MAX ||
(gdb)
936            selectDb(c,dbid) == C_ERR)
(gdb)

3.打印原數據庫和目標數據庫信息,咱們能夠看到原數據庫id爲0,目標數據庫id爲1

(gdb) p *src
$14 = {dict = 0x7f175b80b360, expires = 0x7f175b80b3c0, blocking_keys = 0x7f175b80b420,
  ready_keys = 0x7f175b80b480, watched_keys = 0x7f175b80b4e0, id = 0, avg_ttl = 0,
  defrag_later = 0x7f175b80f330}
(gdb) p *dst
$15 = {dict = 0x7f175b80b540, expires = 0x7f175b80b5a0, blocking_keys = 0x7f175b80b600,
  ready_keys = 0x7f175b80b660, watched_keys = 0x7f175b80b6c0, id = 1, avg_ttl = 0,
  defrag_later = 0x7f175b80f360}

4.在將當前數據庫實例賦值給dst以後切回原數據庫,並判斷目標數據庫和原數據庫是否一致

942        selectDb(c,srcid); /* Back to the source DB */
(gdb)
946        if (src == dst) {

5.查看這個key是否存在,若是存在則返回這個對象,咱們看一下返回的值,發現這個key的值的類型爲0,值爲1,而後獲取他的expire

(gdb) n
952        o = lookupKeyWrite(c->db,c->argv[1]);
(gdb)
953        if (!o) {
(gdb) p o
$2 = (robj *) 0x7f175b80ac80
(gdb) p *o
$3 = {type = 0, encoding = 1, lru = 9180225, refcount = 2147483647, ptr = 0x1}
(gdb) p $3.ptr
$4 = (void *) 0x1
(gdb) p (char*)$3.ptr
$5 = 0x1 <Address 0x1 out of bounds>
(gdb) p (char)$3.ptr
$6 = 1 '\001’
(gdb) n
957        expire = getExpire(c->db,c->argv[1]);

6.接下來是判斷這個key在目標數據庫是否存在,在此由於目標數據庫不存在,跳過if語句

(gdb)
960        if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
7.接下來是向目標數據庫增長這個key,此處過程已經在源碼分析中講解, 故此出只貼出執行流程。
173    void dbAdd(redisDb *db, robj *key, robj *val) {
(gdb) n
174        sds copy = sdsdup(key->ptr);
(gdb)
173    void dbAdd(redisDb *db, robj *key, robj *val) {
(gdb)
174        sds copy = sdsdup(key->ptr);
(gdb)
175        int retval = dictAdd(db->dict, copy, val);
(gdb)
177        serverAssertWithInfo(NULL,key,retval == DICT_OK);
(gdb)
178        if (val->type == OBJ_LIST ||
(gdb)
181        if (server.cluster_enabled) slotToKeyAdd(key);
(gdb)
182    }

8 而後就是判斷是否存在expire。存在則設置,增長引用計數,到此目標數據庫的key已經創建。與此同時,咱們須要刪除原數據庫的key

965        if (expire != -1) setExpire(c,dst,c->argv[1],expire);
(gdb)
966        incrRefCount(o);
(gdb) n
969        dbDelete(src,c->argv[1]);

9.咱們打印目標數據庫的dict,發現kkkk這個剛開始設置的已經存在。而原來的key已經不在。

(gdb) p *dst
$19 = {dict = 0x7f175b80b540, expires = 0x7f175b80b5a0, blocking_keys = 0x7f175b80b600, ready_keys = 0x7f175b80b660,
  watched_keys = 0x7f175b80b6c0, id = 1, avg_ttl = 0, defrag_later = 0x7f175b80f360}
(gdb) p (char*)$19.dict.ht.table.key
$20 = 0x7f175b809931 「kkkk」
(gdb) p (char*)($21.dict.ht.table+0).key
$31 = 0x7f175b809921 "dddd"
(gdb) p (char*)($21.dict.ht.table+2).key
$32 = 0x7f175b8098f9 「key1"

10.最後是響應返回客戶端信息。

拓展

  1. Redis多數據庫:根據咱們講解的move命令能夠看出,redis是多命令的,在move執行時,咱們會進行select
    0來設置數據庫,redis默認是0號數據庫,咱們能夠通縮select命令來選擇數據庫,一個redis實例最多能夠提供16個數據庫,下標分別是從0-15,。命令以下所示:

    select 1
    #選擇鏈接1號數據庫
  2. redis事務,在redis中可使用multi exec discard 這三個命令來實現事務。在事務中,全部命令會被串行化順序執行,事務執行期間redis不會爲其餘客戶端提供任何服務,從而保證事務中的命令都被原子化執行

    • multi 開啓事務,這後邊執行的命令都會被存到命令的隊列當中
    • exec 至關於關係型數據庫事務中的commit,提交事務
    • discard 至關於關係型數據庫事務中的rollback,回滾操做 舉個例子:

      127.0.0.1:6380> set user grape  //設置一個值
      OK
      127.0.0.1:6380> get user
      "grape"
      127.0.0.1:6380> multi  //開啓事務
      OK
      127.0.0.1:6380> set user xiaoming
      QUEUED
      127.0.0.1:6380> discard   //回滾
      OK
      127.0.0.1:6380> get user
      "grape"   // 值不變
      127.0.0.1:6380>
      
      
      
      127.0.0.1:6380> set grape 123  //設置一個值
      OK
      127.0.0.1:6380> multi    //開啓事務
      OK
      127.0.0.1:6380> incr grape 
      QUEUED
      127.0.0.1:6380> exec   //執行事務
      1) (integer) 124
      127.0.0.1:6380> get grape
      "124"    //值改變
      127.0.0.1:6380>
  3. redis鎖

    • 悲觀鎖: 數據被外界修改保守態度(悲觀), 所以, 在整個數據處理過程當中, 將數據處理鎖定狀態. 實現方式: 在對任意記錄修改前, 先嚐試爲該記錄加上排他鎖, 若是加鎖失敗, 說明該記錄正在被修改, 當前查詢可能要等待或拋出異常, 若是成功加鎖, 那麼就能夠對記錄作修改
    • 樂觀鎖: 樂觀鎖假設認爲數據通常狀況下不會形成衝突, 因此在數據進行提交更新的時候, 纔會正式對數據的衝突進行檢測, 若是發現衝突了, 則返回錯誤信息

此處咱們以move命令來分析,假設redis數據庫裏如今有一個key a的值爲10, 同一時刻有兩個redis客戶端(客戶端1, 客戶端2)對a進行了move操做, 那麼結果會如何呢? 咱們發現,後邊那個執行失敗了。可是他並無報錯,爲何呢?在兩個客戶端對同一個key進行操做時有一個前後順序,第一個在進行move以後,第二個在執行時已經沒有這個key了會失敗。這也就是說咱們能夠利用這一特性,將 MOVE 看成鎖(locking)原語(primitive)。在代碼裏咱們能夠來實現鎖,move命令自己是沒有鎖實現的,咱們在源碼裏也並無看到。

127.0.0.1:6380> keys *
1) "dddd"
2) "grape"
3) "key1"
4) "user"
127.0.0.1:6380> move grape 1
(integer) 1
(55.51s)
127.0.0.1:6380>


127.0.0.1:6380> keys *
1) "dddd"
2) "grape"
3) "key1"
4) "user"
127.0.0.1:6380> move grape 1
(integer) 0
(66.41s)

對於redi鎖的實現,建議閱讀:解鎖 Redis 鎖的正確姿式

相關文章
相關標籤/搜索