【Redis5源码学习】浅析redis命令之move篇

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

    原文作者:NoSay
    原文地址: https://segmentfault.com/a/1190000020499577
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞