前言
Redis鏈表經常會被用於訊息佇列的服務,以完成多程式之間的訊息交換。個人認為redis訊息佇列有一個好處,就是可以實現分布式和共用,就和memcache作為mysql的緩衝和mysql內建的緩衝一樣。
鏈表實現訊息佇列
Redis鏈表支援前後插入以及前後取出,所以如果往尾部插入元素,往頭部取出元素,這就是一種訊息佇列,也可以說是消費者/生產者模型。可以利用lpush和rpop來實現。但是有一個問題,如果鏈表中沒有資料,那麼消費者將要在while迴圈中調用rpop,這樣以來就浪費cpu資源,好在Redis提供一種阻塞版pop命令brpop或者blpop,用法為brpop/blpop list timeout, 當鏈表為空白的時候,brpop/blpop將阻塞,直到設定逾時時間到或者list插入一個元素。
用法如下:
charles@charles-Aspire-4741:~/mydir/mylib/redis$ ./src/redis-cli127.0.0.1:6379> lpush list hello(integer) 1127.0.0.1:6379> brpop list 01) "list"2) "hello"127.0.0.1:6379> brpop list 0//阻塞在這裡/* ---------------------------------------------------- *///當我在另一個用戶端lpush一個元素之後,用戶端輸出為127.0.0.1:6379> brpop list 01) "list"2) "world"(50.60s)//阻塞的時間
當鏈表為空白的時候,brpop是阻塞的,等待逾時時間到或者另一個用戶端lpush一個元素。接下來,看下源碼是如何?阻塞brpop命令的。要實現用戶端阻塞,只需要伺服器不給用戶端發送訊息,那麼用戶端就會阻塞在read調用中,等待訊息到達。這是很好實現的,關鍵是如何判斷這個用戶端阻塞的鏈表有資料到達以及通知用戶端解除阻塞?Redis的做法是,將阻塞的鍵以及阻塞在這個鍵上的用戶端鏈表格儲存體在一個字典中,然後每當向資料庫插入一個鏈表時,就判斷這個新插入的鏈表是否有用戶端阻塞,有的話,就解除這個阻塞的用戶端,並且發送剛插入鏈表元素給用戶端,用戶端就這樣解除阻塞。
先看下有關資料結構,以及server和client有關屬性
//阻塞狀態typedef struct blockingState { /* Generic fields. */ mstime_t timeout; /* 逾時時間 */ /* REDIS_BLOCK_LIST */ dict *keys; /* The keys we are waiting to terminate a blocking * operation such as BLPOP. Otherwise NULL. */ robj *target; /* The key that should receive the element, * for BRPOPLPUSH. */ /* REDIS_BLOCK_WAIT */ int numreplicas; /* Number of replicas we are waiting for ACK. */ long long reploffset; /* Replication offset to reach. */} blockingState;//繼續列表typedef struct readyList { redisDb *db;//就緒鍵所在的資料庫 robj *key;//就緒鍵} readyList;//用戶端有關屬性typedef struct redisClient { int btype; /* Type of blocking op if REDIS_BLOCKED. */ blockingState bpop; /* blocking state */}//伺服器有關屬性struct redisServer { /* Blocked clients */ unsigned int bpop_blocked_clients; /* Number of clients blocked by lists */ list *unblocked_clients; /* list of clients to unblock before next loop */ list *ready_keys; /* List of readyList structures for BLPOP & co */}//資料庫有關屬性typedef struct redisDb { //keys->redisCLient映射 dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */ dict *ready_keys; /* Blocked keys that received a PUSH */}redisDB
必須對上述的資料結構足夠瞭解,否則很難看懂下面的代碼,因為這些代碼需要操作上述的資料結構。先從brpop命令執行函數開始分析,brpop命令執行函數為
void brpopCommand(redisClient *c) { blockingPopGenericCommand(c,REDIS_TAIL);}//++++++++++++++++++++++++++++++++++++++++++++++++++void blockingPopGenericCommand(redisClient *c, int where) { robj *o; mstime_t timeout; int j; if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS) != REDIS_OK) return;//將逾時時間儲存在timeout中 for (j = 1; j < c->argc-1; j++) { o = lookupKeyWrite(c->db,c->argv[j]);//在資料庫中尋找操作的鏈表 if (o != NULL) {//如果不為空白 if (o->type != REDIS_LIST) {//不是鏈表類型 addReply(c,shared.wrongtypeerr);//報錯 return; } else { if (listTypeLength(o) != 0) {//鏈表不為空白 /* Non empty list, this is like a non normal [LR]POP. */ char *event = (where == REDIS_HEAD) ? "lpop" : "rpop"; robj *value = listTypePop(o,where);//從鏈表中pop出一個元素 redisAssert(value != NULL); //給用戶端發送pop出來的元素資訊 addReplyMultiBulkLen(c,2); addReplyBulk(c,c->argv[j]); addReplyBulk(c,value); decrRefCount(value); notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event, c->argv[j],c->db->id); if (listTypeLength(o) == 0) {//如果鏈表為空白,從資料庫刪除鏈表 dbDelete(c->db,c->argv[j]); notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del", c->argv[j],c->db->id); } /* 省略一部分 */ } } } } /* 如果鏈表為空白,則阻塞用戶端 */ blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);}
從源碼可以看出,brpop可以操作多個鏈表變數,例如brpop list1 list2 0,但是只能輸出第一個有元素的鏈表。如果list1沒有元素,而list2有元素,則輸出list2的元素;如果兩個都有元素,則輸出list1的元素;如果都沒有元素,則等待其中某個鏈表插入一個元素,之後在2返回。最後調用blockForyKeys阻塞
void blockForKeys(redisClient *c, robj **keys, int numkeys, mstime_t timeout, robj *target) { dictEntry *de; list *l; int j; c->bpop.timeout = timeout;//逾時時間賦值給用戶端blockingState屬性 c->bpop.target = target;//這屬性適用於brpoplpush命令的輸入對象,如果是brpop, //則target為空白 if (target != NULL) incrRefCount(target);//不為空白,增加引用計數 for (j = 0; j < numkeys; j++) { /* 將阻塞的key存入c.bpop.keys字典中 */ if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue; incrRefCount(keys[j]); /* And in the other "side", to map keys -> clients */ //將阻塞的key和用戶端添加進c->db->blocking_keys de = dictFind(c->db->blocking_keys,keys[j]); if (de == NULL) { int retval; /* For every key we take a list of clients blocked for it */ l = listCreate(); retval = dictAdd(c->db->blocking_keys,keys[j],l); incrRefCount(keys[j]); redisAssertWithInfo(c,keys[j],retval == DICT_OK); } else { l = dictGetVal(de); } listAddNodeTail(l,c);//添加到阻塞鍵的客戶點鏈表中 } blockClient(c,REDIS_BLOCKED_LIST);//設定用戶端阻塞標誌}
blockClient函數只是簡單的設定用戶端屬性,如下
void blockClient(redisClient *c, int btype) { c->flags |= REDIS_BLOCKED;//設定標誌 c->btype = btype;//阻塞操作類型 server.bpop_blocked_clients++;}
由於這個函數之後,brpop命令執行函數就結束了,由於沒有給用戶端發送訊息,所以用戶端就阻塞在read調用中。那麼如何解開用戶端的阻塞了?
插入一個元素解阻塞
任何指令的執行函數都是在processCommand函數中調用call函數,然後在call函數中調用命令執行函數,lpush也一樣。當執行完lpush之後,此時鏈表不為空白,回到processCommand調用中,執行以下語句
if (listLength(server.ready_keys)) handleClientsBlockedOnLists();
這兩行代碼是先檢查server.ready_keys是否為空白,如果不為空白,說明已經有一些就緒的鏈表,此時可以判斷是否有用戶端阻塞在這個索引值上,如果有,則喚醒;現在問題又來了,這個server.ready_keys在哪更新鏈表了?
原來是在dbAdd函數中,當往資料庫中添加的實值型別為REDIS-LIST時,這時就要調用signalListAsReady函數將鏈表指標添加進server.ready_keys:
//db.cvoid dbAdd(redisDb *db, robj *key, robj *val) { sds copy = sdsdup(key->ptr); int retval = dictAdd(db->dict, copy, val);//將資料添加進資料庫 redisAssertWithInfo(NULL,key,retval == REDIS_OK); //判斷是否為鏈表類型,如果是,調用有鏈表已經ready函數 if (val->type == REDIS_LIST) signalListAsReady(db, key); if (server.cluster_enabled) slotToKeyAdd(key); }//t_list.cvoid signalListAsReady(redisDb *db, robj *key) { readyList *rl; /* 沒有用戶端阻塞在這個鍵上,則直接返回. */ if (dictFind(db->blocking_keys,key) == NULL) return; /* 這個鍵已近被喚醒了,所以沒必要重新入隊 */ if (dictFind(db->ready_keys,key) != NULL) return; /* Ok, 除了上述兩情況,把這個鍵放入server.ready_keys */ rl = zmalloc(sizeof(*rl)); rl->key = key; rl->db = db; incrRefCount(key); listAddNodeTail(server.ready_keys,rl);//添加鏈表末尾 /* We also add the key in the db->ready_keys dictionary in order * to avoid adding it multiple times into a list with a simple O(1) * check. */ incrRefCount(key); //同時將這個阻塞鍵放入db->ready_keys redisAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);}
OK,這時server.ready_keys上已經有就緒鍵了,這時就調用processCommand函數中的handleClientsBlockedOnLists()函數來處理阻塞用戶端,在這個函數中,
void handleClientsBlockedOnLists(void) { while(listLength(server.ready_keys) != 0) { list *l; /* 將server.ready_keys賦給一個新的list,再將server.ready_keys清空 */ l = server.ready_keys; server.ready_keys = listCreate(); /* 迭代每一個就緒的每一個readyList */ while(listLength(l) != 0) { listNode *ln = listFirst(l);//擷取第一個就緒readyList readyList *rl = ln->value; /* 從rl所屬的資料庫中刪除rl */ dictDelete(rl->db->ready_keys,rl->key); /* 查詢rl所屬的資料庫尋找rl->key ,給阻塞用戶端回複rl->key鏈表中的第一個元素*/ robj *o = lookupKeyWrite(rl->db,rl->key); if (o != NULL && o->type == REDIS_LIST) { dictEntry *de; /* 在rl->db->blocking_keys尋找阻塞在rl->key的用戶端鏈表 */ de = dictFind(rl->db->blocking_keys,rl->key); if (de) { list *clients = dictGetVal(de);//轉換為用戶端鏈表 int numclients = listLength(clients); while(numclients--) {//給每個用戶端發送訊息 listNode *clientnode = listFirst(clients); redisClient *receiver = clientnode->value;//阻塞的用戶端 robj *dstkey = receiver->bpop.target;//brpoplpush命令目的鏈表 int where = (receiver->lastcmd && receiver->lastcmd->proc == blpopCommand) ? REDIS_HEAD : REDIS_TAIL;//擷取取出的方向 robj *value = listTypePop(o,where);//取出就緒鏈表的元素 if (value) { /* Protect receiver->bpop.target, that will be * freed by the next unblockClient() * call. */ if (dstkey) incrRefCount(dstkey); unblockClient(receiver);//設定用戶端為非阻塞狀態 if (serveClientBlockedOnList(receiver, rl->key,dstkey,rl->db,value, where) == REDIS_ERR) { /* If we failed serving the client we need * to also undo the POP operation. */ listTypePush(o,value,where); }//給用戶端回複鏈表中的元素內容 if (dstkey) decrRefCount(dstkey); decrRefCount(value); } else { break; } } } //如果鏈表為空白,則從資料庫中刪除 if (listTypeLength(o) == 0) dbDelete(rl->db,rl->key); /* We don't call signalModifiedKey() as it was already called * when an element was pushed on the list. */ } /* 回收rl */ decrRefCount(rl->key); zfree(rl); listDelNode(l,ln); } listRelease(l); /* We have the new list on place at this point. */ }}
從這個源碼可知,如果有兩個用戶端,同時阻塞在一個鏈表上面,那麼如果鏈表插入一個元素之後,只有先阻塞的那個用戶端收到訊息,後面阻塞的那個用戶端繼續阻塞,這也是先阻塞先服務的思想。handleClientsBlockedOnLists函數調用了unblockClient(receiver) ,該函數功能為接觸用戶端阻塞標誌,以及找到db阻塞在key上的用戶端鏈表,並將接觸阻塞的用戶端從鏈表刪除。然後調用serveClientBlockOnList給用戶端回複剛在鏈表插入的元素。
int serveClientBlockedOnList(redisClient *receiver, robj *key, robj *dstkey, redisDb *db, robj *value, int where){ robj *argv[3]; if (dstkey == NULL) { /* Propagate the [LR]POP operation. */ argv[0] = (where == REDIS_HEAD) ? shared.lpop : shared.rpop; argv[1] = key; propagate((where == REDIS_HEAD) ? server.lpopCommand : server.rpopCommand, db->id,argv,2,REDIS_PROPAGATE_AOF|REDIS_PROPAGATE_REPL); /* BRPOP/BLPOP */ addReplyMultiBulkLen(receiver,2); addReplyBulk(receiver,key); addReplyBulk(receiver,value); } else { /* BRPOPLPUSH */ /* 省略 */ }}
propagate函數主要是將命令資訊發送給aof和slave。函數中省略部分是brpoplpush list list1 0命令的目的鏈表list1非空時,將從list鏈表pop出來的元素插入list1中。當給用戶端發送訊息之後,用戶端就從read函數調用中返回,變為不阻塞。
通過逾時時間解阻塞
如果鏈表一直沒有資料插入,那麼用戶端將會一直阻塞下去,這肯定是不行的,所以brpop還支援逾時阻塞,即阻塞時間超過一定值之後,伺服器返回一個空值,這樣用戶端就解脫阻塞了。
對於時間逾時,都放在了100ms執行一次的時間事件中;逾時解脫阻塞函數也在serverCron中;在serverCron->clientsCron->clientsCronHandleTimeout
int clientsCronHandleTimeout(redisClient *c, mstime_t now_ms) { time_t now = now_ms/1000; //.......... else if (c->flags & REDIS_BLOCKED) { /* Blocked OPS timeout is handled with milliseconds resolution. * However note that the actual resolution is limited by * server.hz. */ if (c->bpop.timeout != 0 && c->bpop.timeout < now_ms) { /* Handle blocking operation specific timeout. */ replyToBlockedClientTimedOut(c); unblockClient(c); } } //.............
把這個函數不相干的代碼刪除,主要部分先判斷這個用戶端是否阻塞,如果是,逾時時間是否到期,如果是,則調用replyToBlockedClientTimedOut給用戶端回複一個空回複,以及接觸用戶端阻塞。
總結
鏈表訊息佇列實現暫時分析到這了,大家都學會了嗎?希望這篇文章給大家能帶來一定的協助,如果有疑問可以留言交流。