順風車運營研發團隊 李樂
1.命令執行過程
1.1命令請求格式
當使用者在客戶端鍵入一條命令請求時,客戶端會將其按照特定協議轉換為字串,傳送給伺服器;伺服器解析字串,獲取命令請求;
例如,當使用者執行 set key value 時,轉換後的字串為 *3rn3rnset3rnkey$5rnvaluern
其中,*3表示當前命令請求引數數目(set命令也是一個引數);rn用於分隔每個引數;3、5等表示引數字串長度;
1.2 服務端讀取命令請求
1.2.1客戶端處理函式簡介:
伺服器啟動時,會監聽socket,並建立對應檔案事件,監聽此fd上的可讀事件;(server.c/initServer)
//監聽socket
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
//為所有監聽的socket建立檔案事件,監聽可讀事件;事件處理函式為acceptTcpHandler
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
}
}
當客戶端連線到伺服器時,會呼叫acceptTcpHandler處理函式,伺服器會為每個連結建立一個client物件,並建立相應檔案事件,監聽此連結fd的可讀事件,並指定事件處理函式
//接收客戶端連結請求
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
}
//建立客戶端
if ((c = createClient(fd)) == NULL) {
close(fd); /* May be already closed, just ignore errors */
return;
}
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
//設定fd為非阻塞;非延遲(epoll等堅挺的額socket必須是非阻塞;延遲的話傳送的資料會先儲存在tcp緩衝區,等到一定事件或資料量大時才會傳送)
//建立檔案事件,監聽可讀事件,事件處理函式為readQueryFromClient
if (fd != -1) {
anetNonBlock(NULL,fd);
anetEnableTcpNoDelay(NULL,fd);
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}
……………………//初始化client結構體各欄位
}
1.2.2 讀取命令請求到輸入緩衝區
命令請求字串會先讀入client的輸入緩衝區中
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
…………
qblen = sdslen(c->querybuf);
//統計
if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
//讀取
nread = read(fd, c->querybuf+qblen, readlen);
//處理輸入緩衝區
processInputBuffer(c);
…………
}
1.2.3 解析輸入緩衝區資料
首先呼叫processMultibulkBuffer:解析*3獲取行數,迴圈獲取每一行引數(會先解析$3獲取引數長度),構造為一個redisObject物件,儲存在客戶端結構體的argv和argc欄位
其次呼叫processCommand處理命令請求
void processInputBuffer(client *c) {
while(sdslen(c->querybuf)) {
//判斷命令請求型別;telnet傳送的命令和redis-cli傳送的命令請求格式不同
if (!c->reqtype) {
if (c->querybuf[0] == `*`) {
c->reqtype = PROTO_REQ_MULTIBULK;
} else {
c->reqtype = PROTO_REQ_INLINE;
}
}
if (c->reqtype == PROTO_REQ_INLINE) {
if (processInlineBuffer(c) != C_OK) break;
} else if (c->reqtype == PROTO_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != C_OK) break;
} else {
serverPanic("Unknown request type");
}
//引數個數為0時
if (c->argc == 0) {
resetClient(c);
} else {
//處理命令
if (processCommand(c) == C_OK) {
if (c->flags & CLIENT_MASTER && !(c->flags & CLIENT_MULTI)) {
if (!(c->flags & CLIENT_BLOCKED) || c->btype != BLOCKED_MODULE)
resetClient(c);
}
if (server.current_client == NULL) break;
}
}
}
//resetClient()函式會釋放client結構體arg欄位中的各引數,重置argc為0
解析緩衝區字串邏輯如下:
int processMultibulkBuffer(client *c) {
char *newline = NULL;
long pos = 0;
int ok;
long long ll;
if (c->multibulklen == 0) {
//定位到第一行結束
newline = strchr(c->querybuf,`
`);
//解析行數,即引數個數
ok = string2ll(c->querybuf+1,newline-(c->querybuf+1),&ll);
c->multibulklen = ll;
//argv村春命令引數,解析之前先清空
if (c->argv) zfree(c->argv);
c->argv = zmalloc(sizeof(robj*)*c->multibulklen);
}
//迴圈解析所有引數
while(c->multibulklen) {
//讀取$3字串,解析數值3
if (c->bulklen == -1) {
newline = strchr(c->querybuf+pos,`
`);
if (c->querybuf[pos] != `$`) {
return C_ERR;
}
ok = string2ll(c->querybuf+pos+1,newline-(c->querybuf+pos+1),&ll);
c->bulklen = ll;
}
//讀取引數
c->argv[c->argc++] =
createStringObject(c->querybuf+pos,c->bulklen);
pos += c->bulklen+2;
c->bulklen = -1;
c->multibulklen--;
}
/* We`re done when c->multibulk == 0 */
if (c->multibulklen == 0) return C_OK;
/* Still not ready to process the command */
return C_ERR;
}
1.2.4 補充:什麼是redisObject
redis底層有多種資料結構:sds,intset,ziplist,linkedlist,skiplist,dict等;redis對外提供的資料型別有字串、列表、雜湊表、有需集合、集合;
redis會根據實際情況選擇合適的資料結構來儲存某一種資料型別;而同一種資料型別可能使用不同的資料結構儲存;
redisObject是對底層多種資料結構的進一步封裝;看看結構體:
typedef struct redisObject {
unsigned type:4; //資料型別:字串、列表、雜湊表、集合、有序集合
unsigned encoding:4; //儲存資料型別使用哪種資料結構,如列表的實現可能是ziplist或linkedlist
unsigned lru:LRU_BITS; //lru,資料淘汰
int refcount; //引用計數
void *ptr; //指向具體的資料結構
} robj;
1.2.5 處理命令
processInputBuffer解析輸入緩衝區中命令請求字串,將各個引數轉換為redisObject儲存在client結構體的argv欄位中,argc儲存引數個數,下一步呼叫processCommand處理命令
1.2.5.1命令結構體redisCommand
struct redisCommand {
char *name; //命令名稱
redisCommandProc *proc; //命令處理函式指標
int arity; //命令引數個數,用於檢查命令請求引數是否正確;當取值-N時,表示引數個數大於等於N
char *sflags; //標識命令屬性;如讀命令或寫命令等
int flags; //sflags的二進位制表示
//沒搞明白
redisGetKeysProc *getkeys_proc;
int firstkey;
int lastkey;
int keystep;
//命令執行總耗時和執行次數
long long microseconds, calls;
};
1.2.5.2命令執行前校驗
int processCommand(client *c) {
//quit命令直接返回
if (!strcasecmp(c->argv[0]->ptr,"quit")) {
addReply(c,shared.ok);
c->flags |= CLIENT_CLOSE_AFTER_REPLY;
return C_ERR;
}
//命令字典查詢指定命令;所有的命令都儲存在命令字典中 struct redisCommand redisCommandTable[]={}
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
if (!c->cmd) { //沒有查詢到命令
} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) || (c->argc < -c->cmd->arity)) { //命令請求引數個數錯誤
}
//是否通過認證;沒有通過且必須認證時,只接受認證命令
if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand){
}
//開啟最大記憶體限制
if (server.maxmemory) {
//釋放記憶體
int retval = freeMemoryIfNeeded();
//CMD_DENYOOM表示記憶體不夠時,禁止執行此命令
if ((c->cmd->flags & CMD_DENYOOM) && retval == C_ERR) {
}
//當此伺服器是master時:aof持久化失敗時,或上一次bgsave執行錯誤,且配置bgsave引數和stop_writes_on_bgsave_err;禁止執行寫命令
if (((server.stop_writes_on_bgsave_err &&
server.saveparamslen > 0 &&
server.lastbgsave_status == C_ERR) ||
server.aof_last_write_status == C_ERR) &&
server.masterhost == NULL &&
(c->cmd->flags & CMD_WRITE ||
c->cmd->proc == pingCommand)){
}
//當此伺服器時master時:如果配置了repl_min_slaves_to_write,當slave數目小於時,禁止執行寫命令
if (server.masterhost == NULL &&
server.repl_min_slaves_to_write &&
server.repl_min_slaves_max_lag &&
c->cmd->flags & CMD_WRITE &&
server.repl_good_slaves_count < server.repl_min_slaves_to_write){
}
//當此伺服器是slave,且配置了只讀時,如果客戶端不是master,則拒絕執行寫命令
if (server.masterhost && server.repl_slave_ro &&
!(c->flags & CLIENT_MASTER) &&
c->cmd->flags & CMD_WRITE){
}
//當客戶端正在訂閱頻道時,只會執行以下命令
if (c->flags & CLIENT_PUBSUB &&
c->cmd->proc != pingCommand &&
c->cmd->proc != subscribeCommand &&
c->cmd->proc != unsubscribeCommand &&
c->cmd->proc != psubscribeCommand &&
c->cmd->proc != punsubscribeCommand) {
}
//伺服器為slave,但沒有正確連線master時,只會執行帶有CMD_STALE標誌的命令,如info等
if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED &&
server.repl_serve_stale_data == 0 &&
!(c->cmd->flags & CMD_STALE)){
}
//正在載入資料庫時,只會執行帶有CMD_LOADING標誌的命令,其餘都會被拒絕
if (server.loading && !(c->cmd->flags & CMD_LOADING)) {
}
//當伺服器因為執行lua指令碼阻塞時,只會執行以下幾個命令,其餘都會拒絕
if (server.lua_timedout &&
c->cmd->proc != authCommand &&
c->cmd->proc != replconfCommand &&
!(c->cmd->proc == shutdownCommand &&
c->argc == 2 &&
tolower(((char*)c->argv[1]->ptr)[0]) == `n`) &&
!(c->cmd->proc == scriptCommand &&
c->argc == 2 &&
tolower(((char*)c->argv[1]->ptr)[0]) == `k`)){
}
//執行命令
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand){
//開啟了事務,命令只會入佇列;
queueMultiCommand(c);
addReply(c,shared.queued);
} else {
//直接執行命令
call(c,CMD_CALL_FULL);
}
}
1.2.5.3 命令執行
執行命令時,會需要做很多額外的操作,統計,記錄慢查詢日誌,傳播命令道monitor、slave,aof持久化等
//flags=CMD_CALL_FULL=(CMD_CALL_SLOWLOG | CMD_CALL_STATS | CMD_CALL_PROPAGATE)
//表示需要記錄慢查詢日誌,統計,廣播命令
void call(client *c, int flags) {
//dirty記錄資料庫修改次數;start記錄命令開始執行時間us;duration記錄命令執行花費時間
long long dirty, start, duration;
int client_old_flags = c->flags;
//有監視器的話,需要將命令傳送給監視器
if (listLength(server.monitors) &&
!server.loading &&
!(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN)))
{
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
//處理命令,呼叫命令處理函式
dirty = server.dirty;
start = ustime();
c->cmd->proc(c);
duration = ustime()-start;
dirty = server.dirty-dirty;
if (dirty < 0) dirty = 0;
//記錄慢查詢日誌
if (flags & CMD_CALL_SLOWLOG && c->cmd->proc != execCommand) {
char *latency_event = (c->cmd->flags & CMD_FAST) ?
"fast-command" : "command";
latencyAddSampleIfNeeded(latency_event,duration/1000);
slowlogPushEntryIfNeeded(c,c->argv,c->argc,duration);
}
//統計
if (flags & CMD_CALL_STATS) {
c->lastcmd->microseconds += duration;
c->lastcmd->calls++;
}
//廣播命令
if (flags & CMD_CALL_PROPAGATE &&
(c->flags & CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)
{
int propagate_flags = PROPAGATE_NONE;
//dirty大於0時,需要廣播命令給slave和aof
if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
if (c->flags & CLIENT_FORCE_REPL) propagate_flags |= PROPAGATE_REPL;
if (c->flags & CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;
if (c->flags & CLIENT_PREVENT_REPL_PROP ||
!(flags & CMD_CALL_PROPAGATE_REPL))
propagate_flags &= ~PROPAGATE_REPL;
if (c->flags & CLIENT_PREVENT_AOF_PROP ||
!(flags & CMD_CALL_PROPAGATE_AOF))
propagate_flags &= ~PROPAGATE_AOF;
//廣播命令,寫如aof,傳送命令到slave
if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
}
}
2.命令表
redis有個命令表redisCommandTable儲存每個命令的詳細屬性
//命令名稱,命令處理函式,命令引數(-3表示引數數目大於等於3個),命令屬性標誌,…………
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
{"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
{"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
{"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
{"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},
{"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
…………
}
3.set命令執行
3.1 set命令介紹
SET key value [EX seconds] [PX milliseconds] [NX|XX]
將字串值 value 關聯到 key 。
如果 key 已經持有其他值, SET 就覆寫舊值,無視型別。
對於某個原本帶有生存時間(TTL)的鍵來說, 當 SET 命令成功在這個鍵上執行時, 這個鍵原有的 TTL 將被清除。
EX、PX、NX/XX可選引數含義如下:
- EX second :設定鍵的過期時間為 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。
- PX millisecond :設定鍵的過期時間為 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX keymillisecond value 。
- NX :只在鍵不存在時,才對鍵進行設定操作。 SET key value NX 效果等同於 SETNX key value 。
- XX :只在鍵已經存在時,才對鍵進行設定操作。
3.2 set命令執行函式
3.2.1 解析set命令請求
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
int j;
robj *expire = NULL; //過期時間
int unit = UNIT_SECONDS; //時間單位
int flags = OBJ_SET_NO_FLAGS; //標誌命令是否攜帶nx、xx、ex、px可選引數
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] == ` ` &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX; //NX標誌
} else if ((a[0] == `x` || a[0] == `X`) &&
(a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX; //XX標誌
} else if ((a[0] == `e` || a[0] == `E`) &&
(a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX; //EX標誌
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == `p` || a[0] == `P`) &&
(a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX; //PX標誌
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL); //處理命令
}
3.2.2 命令處理
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 */
//設定了過期時間;expire是robj型別,獲取整數值
if (expire) {
if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
return;
if (milliseconds <= 0) {
addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
return;
}
if (unit == UNIT_SECONDS) milliseconds *= 1000;
}
//NX,key存在時直接返回;XX,key不存在時直接返回
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;
}
//新增都資料庫字典
setKey(c->db,key,val);
server.dirty++;
//過期時間新增到過期字典
if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
//鍵空間通知
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
"expire",key,c->db->id);
addReply(c, ok_reply ? ok_reply : shared.ok);
}