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; }
首先設置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.最後是響應返回客戶端信息。
Redis多數據庫:根據咱們講解的move命令能夠看出,redis是多命令的,在move執行時,咱們會進行select
0來設置數據庫,redis默認是0號數據庫,咱們能夠通縮select命令來選擇數據庫,一個redis實例最多能夠提供16個數據庫,下標分別是從0-15,。命令以下所示:
select 1 #選擇鏈接1號數據庫
redis事務,在redis中可使用multi exec discard 這三個命令來實現事務。在事務中,全部命令會被串行化順序執行,事務執行期間redis不會爲其餘客戶端提供任何服務,從而保證事務中的命令都被原子化執行
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>
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 鎖的正確姿式