redis源码阅读4----SDS

本来这章的标题是想写string类型的操作的。

但为什么改成SDS了哪?

因为我之前粗俗的认为redis只有string类型有用到SDS。但是在写这篇博客之前,稍微翻了下其他地方的源码,发现redis用到SDS的地方实际上是很多的。

所以将SDS认为只是string类型的底层结构其实是狭隘的。

所以,与其只是讨论string,不如进一步写一下SDS。当然,本章主要还是讨论在string类型下的SDS,其他数据类型先放一放。

 

以往的文章都是直接打断点,讨论运行过程。而没有去讨论对应的结构体对象或者说是其存储结构。但是今天我想先讨论下SDS的底层结构,先附上sds.h下的struct结构:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
 
sdshdr5这个结构体请忽略,官方明确的说明不会在程序中使用。只是在这里记录5类SDS的布局。
很明显5类SDS的区别只是在于存储大小。并且每一类都是做了内存对齐。
 
buf[]很明显是用于存储字符串的char类型数组。
flags存储sds类型,只用前三位,后五位预留。
len是已经使用了长度。
alloc是空闲的长度。
 
然后再说一下redisObject这个struct:
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;
redis的每个k-v都是由这个结构体演化而来的。
 
 
好了,接下来我们来看看源码:
以 set a a为例。
我建议将断点打在t_string.c/setCommad下(266行)
先给大家看一看目前的堆栈:

 

 至于connSocketEventHandler在哪注册的,请看如下:

static int connSocketConnect(connection *conn, const char *addr, int port, const char *src_addr,
        ConnectionCallbackFunc connect_handler) {
    int fd = anetTcpNonBlockBestEffortBindConnect(NULL,addr,port,src_addr);
    if (fd == -1) {
        conn->state = CONN_STATE_ERROR;
        conn->last_errno = errno;
        return C_ERR;
    }

    conn->fd = fd;
    conn->state = CONN_STATE_CONNECTING;

    conn->conn_handler = connect_handler;
    aeCreateFileEvent(server.el, conn->fd, AE_WRITABLE,
            conn->type->ae_handler, conn);

    return C_OK;
}
注册完成后
aeProcessEvents下fe->rfileProc(eventLoop,fd,fe->clientData,mask);就直接调用了。和上篇文章的一样。
这是个很简单的点。不过为了防止自己忘记是在哪注册的,还是记一下。
 
 
进入setCommad
首先声明一个redisobject结构体对象。
robj *expire = NULL;
初始化两个变量,暂时不知道是干啥的:
int unit = UNIT_SECONDS;
    int flags = OBJ_NO_FLAGS;
两个值都为0.
 
进入第一个函数:
parseExtendedStringArgumentsOrReply
这个函数首先是检查语法,如果语法错了,直接返回err,那么setCommad会直接return。
因为我在这里只是简单的执行了set a a
所以在第一次判断时:
int j = command_type == COMMAND_GET ? 2 : 3;
    for (; j < c->argc; j++) {
就直接不用进入循环了,整个流程就回到setCommad下了。
不过循环中的解析是比较简单的,也可以从这个循环看出redis是完全解析大小写的,也就是说一条commad,你哪怕是大小写混着用,他也能完全解析的。
 
接下来是
c->argv[2] = tryObjectEncoding(c->argv[2]);
这个函数会对argv[2]进行字符串编码,以节省空间。
sds s = o->ptr;首先把key放入sds中。
将此sds定义为string对象:
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
然后计算一下字符串大小:
len = sdslen(s);很明显这里只会用到sdshdr8。
 
以下部分是,如果字符串很小并且是raw编码,那么会使用EMBSTR编码。并且对象和SDS字符串分配在同一内存块中,以节省空间和缓存未命中。
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }
 
setCommad的最后一个函数:
setGenericCommand
其中:
genericSetKey:高级别的设置操作,这个操作可以将key(无论是否存在)设置到新的对象。
首先调用:
lookupKeyWrite。这个函数很复杂,主要是做以下操作:查看key的ttl是否到期。这个确实挺惊讶,看了下代码,只要是对key有任何操作,都会去确认他的ttl,哪怕你是刚刚set。所以如果ttl是过期了,那么key会被逐出。此外,这还可能会触发在AOF/复制中传播DEL/UNLINK命令。
另外此函数的行为取决于实例的复制角色,因为从实例不会使密钥过期(del),所以它们会等待主实例的DEL来实现一致性。然而,即使从机也会尝试为函数提供一致的返回值,这样,在从机端执行的读取命令将能够像key过期一样运行,即使key仍然存在(因为主机尚未传播DEL)。
如果key仍然有效,则函数的返回值为0;否则,如果key过期,则函数返回1。
 
给string分配内存。
if (!(flags & OBJ_SET_GET)) {
        addReply(c, ok_reply ? ok_reply : shared.ok);
    }
 
querybuffer:
0x7ffff6d89645 "*3\r\n$3\r\nset\r\n$1\r\na\r\n$1\r\na\r\n"。
 
 
 
简单的来说,当一条set被执行时。redis会调用读处理器,即readQueryFromClient。用来处理此次从客户端收到的信息。如果解析是正确的,那么根据解析出来的不同命令,去执行不同的回调函数,比如set就会执行setCommad。
而readQueryFromClient的核心是processInputBuffer,处理函数的最后面,用来对命令进行解析。然后会做如下判断:如果客户端被阻塞了,bread。有客户端的命令已经完全解析,并能执行,break。如果是从库,如果很繁忙,那么暂时不执行命令,只是积累复制,break。如果客户端关闭了,break。
然后判断查询类型:
if (!c->reqtype) {
            if (c->querybuf[c->qb_pos] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }
最后解析命令,执行命令,重置客户端。
解析命令在以下函数:
processMultibulkBuffer。
执行命令在以下函数:
processCommandAndResetClient。
 
刚刚我贴了下querybuffer,processMultibulkBuffer就是用于解析这个的。开头的*表示这个的查询类型应该调用processMultibulkBuffer。\r\n为间隔符。$1表示后面字符串的数量。set是3,a是1.
在processMultibulkBuffer中,第一个while是解析出参数的个数,,本章案例中是3.
然后在第二个while中是将各个参数创建进c->argv。然后从querybuffer中删除,解析完成就返回,如果解析完还有剩余的东西,就说明协议错误,返回err。
 
然后是执行函数:processCommandAndResetClient下的processCommand。
首先是处理quit。
然后根据argv[0]在redis command字典中寻找命令,然后检查命令的合法性以及命令参数是否和之前解析出来的个数相符合。然后将命令存入c->cmd。并更新last command。
然后检查AUTH权限。(我一直觉得检查权限这件事情不是应该很早就做了吗。这里挺疑惑的。这么久才检查权限,感觉很浪费资源。)
然后检查acl,这部分跳过。
接着是集群的检查。如果开启了集群,那么会在这里完成命令的重定向。除非执行命令的服务端本身是master或者命令没有key参数。
然后检查maxmemory。如果内存超过限制,那么会通过删除过期键的方式来释放内存。如果执行的命令会占用大量内存(有个flag:is_denyoom_command来判断),并且之前删除过期键来释放内存失败了,会向客户端返回错误。
然后就是一些lua脚本的内容。
最后就是执行命令:
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand &&
        c->cmd->proc != resetCommand)
    {
        queueMultiCommand(c);      //除了exec,discard,multi,watch,reset之外的命令都会入队。
        addReply(c,shared.queued);
    } else {
        call(c,CMD_CALL_FULL);      //调用call执行命令,其中也会写入慢日志,aof,以及更新命令的执行时间,和一些客户端信息。
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
 
posted @   拿什么救赎  阅读(70)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示