Grape
命令语法
命令含义:将当前数据库的 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 。
源码分析
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。
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命令
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:
(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
(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不会为其他客户端提供任何服务,从而保证事务中的命令都被原子化执行
- 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>
-
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 锁的正确姿势