目录

介绍完Redis的底层数据结构之后, 介绍我们平时使用Redis的时候可以直接看到五种数据结构:字符串、哈希、链表、集合和有序集合。

首先介绍字符串t_string的实现。

字符串的结构

上一篇文章讲到过字符串的底层实现其实有三种编码:

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象。
REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象。
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象。

这三种类型分别对应的底层数据结构为int,embstr, sds。

字符串的定义代码如下:

typedef struct redisObject {
    //对象的数据类型,字符串对象应该为 OBJ_STRING
    unsigned type:4;        
    //对象的编码类型,分别为OBJ_STRING、OBJ_ENCODING_INT或OBJ_ENCODING_EMBSTR
    unsigned encoding:4;
    //LRU_BITS为24位,最近一次的访问时间
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    //引用计数
    int refcount;
    //指向底层数据实现的指针
    void *ptr;
} robj;

那么字符串具体用哪种编码实现呢?

如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int 。

如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw 。

如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。

embstr 编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject 结构和 sdshdr 结构来表示字符串对象, 但 raw 编码会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构, 而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构

这边采用int和embstr编码的原因是:相较于raw格式的2次分配内存, 这两种格式只需要一次分配内存空间就可以;并且在回收的时候,也只需要调用一次内存释放函数。在存储小的字符串的时候更有速度优势。

字符串命令

字符串支持的命令如下:

命令 命令描述
SET key value [ex 秒数][px 毫秒数][nx/xx] 设置指定key的值
GET key 获取指定key的值
APPEND key value 将value追加到指定key的值末尾
INCRBY key increment 将指定key的值加上增量increment
DECRBY key decrement 将指定key的值减去增量decrement
STRLEN key 返回指定key的值长度
SETRANGE key offset value 将value覆写到指定key的值上,从offset位开始
GETRANGE key start end 获取指定key中字符串的子串[start,end]
MSET key value [key value …] 一次设定多个key的值
MGET key1 [key2..] 一次获取多个key的值

字符串命令实现

set命令的实现

set命令用于设置指定的值,其具体命令格式如下:

set key value [ex 秒数] [px 毫秒数] [nx/xx]

其中,各个选项的含义如下:

  • ex 设置指定的到期时间,单位为秒
  • px 设置指定的到期时间,单位为毫秒
  • nx 只有在key不存在的时候,才设置key的值
  • xx 只有key存在时,才对key进行设置操作

set命令是调用setCommand实现的:

// 关于set命令的操作有三种宏定义
#define OBJ_SET_NO_FLAGS 0    // 没有设定参数
#define OBJ_SET_NX (1<<0)     // 只有键不存在时才设定其值
#define OBJ_SET_XX (1<<1)      // 只有键存在时才设定其值
#define OBJ_SET_EX (1<<2)       // ex属性,到期时间单位为秒
#define OBJ_SET_PX (1<<3)     	// px属性,到期时间单位为毫秒

/* set命令实现函数 */
void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
  	// 用于标记ex/px和nx/xx命令参数
    int flags = OBJ_SET_NO_FLAGS;
	// 从命令串的第四个参数开始,查看其是否设定了ex/px和nx/xx
    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
            !(flags & OBJ_SET_XX)) // 标记
        {
            flags |= OBJ_SET_NX;
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_NX))
        {
            flags |= OBJ_SET_XX;
        } else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_PX) && next)
        {
            flags |= OBJ_SET_EX;
            unit = UNIT_SECONDS;
            expire = next;
            j++;
        } else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
                   !(flags & OBJ_SET_EX) && next)
        {
            flags |= OBJ_SET_PX;
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
          	// 如果不是上述参数,则需要报错,命令错误
            addReply(c,shared.syntaxerr);
            return;
        }
    }
    // 判断value是否可以编码成整数,如果能则编码;反之不做处理
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    // 调用底层函数进行键值对设定
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

函数的一开始先检查传入的参数,看是否设定了过期时间以及过期时间的精度等,然后设置对应的flag, 最后根据flag调用setGenericCommand实现具体的操作。

接下去看一下setGenericCommand函数的实现:

//setGenericCommand()函数是以下命令: SET, SETEX, PSETEX, SETNX.的最底层实现
//flags 可以是NX或XX,由上面的宏提供
//expire 定义key的过期时间,格式由unit指定
//ok_reply和abort_reply保存着回复client的内容,NX和XX也会改变回复
//如果ok_reply为空,则使用 "+OK"
//如果abort_reply为空,则使用 "$-1"
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */ //初始化,避免错误

    //如果定义了key的过期时间
    if (expire) {
        //从expire对象中取出值,保存在milliseconds中,如果出错发送默认的信息给client
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
        // 如果过期时间小于等于0,则发送错误信息给client
        if (milliseconds <= 0) {
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        //如果unit的单位是秒,则需要转换为毫秒保存
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }

    //lookupKeyWrite函数是为执行写操作而取出key的值对象
    //如果设置了NX(不存在),并且在数据库中 找到 该key,或者
    //设置了XX(存在),并且在数据库中 没有找到 该key
    //回复abort_reply给client
    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.nullbulk);
        return;
    }
    //在当前db设置键为key的值为val
    setKey(c->db,key,val);

    //设置数据库为脏(dirty),服务器每次修改一个key后,都会对脏键(dirty)增1
    server.dirty++;

    //设置key的过期时间
    //mstime()返回毫秒为单位的格林威治时间
    if (expire) setExpire(c->db,key,mstime()+milliseconds);

    //发送"set"事件的通知,用于发布订阅模式,通知客户端接受发生的事件
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);

    //发送"expire"事件通知
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
        "expire",key,c->db->id);

    //设置成功,则向客户端发送ok_reply
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

get命令的实现

类似于set命令,get命令也是最终调用一个getGenericcommand的函数实现:

//GET 命令的底层实现
int getGenericCommand(client *c) {
    robj *o;

    //lookupKeyReadOrReply函数是为执行读操作而返回key的值对象,找到返回该对象,找不到会发送信息给client
    //如果key不存在直接,返回0表示GET命令执行成功
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return C_OK;

    //如果key的值的编码类型不是字符串对象
    if (o->type != OBJ_STRING) {
        addReply(c,shared.wrongtypeerr);    //返回类型错误的信息给client,返回-1表示GET命令执行失败
        return C_ERR;
    } else {
        addReplyBulk(c,o);  //返回之前找到的对象作为回复给client,返回0表示GET命令执行成功
        return C_OK;
    }
}

编码的转换

上边说到字符串类型底层其实有三种数据类型,这三种数据类型在特定的情况下也会互相转换。

int编码转换为raw编码

对于 int 编码的字符串对象来说, 如果我们向对象执行了一些命令, 使得这个对象保存的不再是整数值, 而是一个字符串值, 比如在最开始的数字类型后边执行了一个append的操作,加上了一串字符串,那么字符串对象的编码将从 int 变为 raw 。

embstr编码转换为raw编码

因为 Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序 (只有 int 编码的字符串对象和 raw 编码的字符串对象有这些程序), 所以 embstr 编码的字符串对象实际上是只读的: 当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令; 因为这个原因, embstr 编码的字符串对象在执行修改命令之后, 总会变成一个 raw 编码的字符串对象。