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 锁的正确姿式