国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務器之家:專注于服務器技術及軟件下載分享
分類導航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|數據庫技術|

服務器之家 - 數據庫 - Redis - 詳解Redis用鏈表實現消息隊列

詳解Redis用鏈表實現消息隊列

2019-10-30 19:08daisy Redis

Redis有兩種方式實現消息隊列,一種是用Redis自帶的鏈表數據結構,另一種是用Redis發布/訂閱模式實現,這篇文章先介紹鏈表實現消息隊列,有需要的朋友們可以參考借鑒。

前言

Redis鏈表經常會被用于消息隊列的服務,以完成多程序之間的消息交換。個人認為redis消息隊列有一個好處,就是可以實現分布式和共享,就和memcache作為mysql的緩存和mysql自帶的緩存一樣。

鏈表實現消息隊列

Redis鏈表支持前后插入以及前后取出,所以如果往尾部插入元素,往頭部取出元素,這就是一種消息隊列,也可以說是消費者/生產者模型。可以利用lpush和rpop來實現。但是有一個問題,如果鏈表中沒有數據,那么消費者將要在while循環中調用rpop,這樣以來就浪費cpu資源,好在Redis提供一種阻塞版pop命令brpop或者blpop,用法為brpop/blpop list timeout, 當鏈表為空的時候,brpop/blpop將阻塞,直到設置超時時間到或者list插入一個元素。

用法如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
charles@charles-Aspire-4741:~/mydir/mylib/redis$ ./src/redis-cli
127.0.0.1:6379> lpush list hello
(integer) 1
127.0.0.1:6379> brpop list 0
1) "list"
2) "hello"
127.0.0.1:6379> brpop list 0
//阻塞在這里
/* ---------------------------------------------------- */
//當我在另一個客戶端lpush一個元素之后,客戶端輸出為
127.0.0.1:6379> brpop list 0
1) "list"
2) "world"
(50.60s)//阻塞的時間

當鏈表為空的時候,brpop是阻塞的,等待超時時間到或者另一個客戶端lpush一個元素。接下來,看下源碼是如何實現阻塞brpop命令的。要實現客戶端阻塞,只需要服務器不給客戶端發送消息,那么客戶端就會阻塞在read調用中,等待消息到達。這是很好實現的,關鍵是如何判斷這個客戶端阻塞的鏈表有數據到達以及通知客戶端解除阻塞?Redis的做法是,將阻塞的鍵以及阻塞在這個鍵上的客戶端鏈表存儲在一個字典中,然后每當向數據庫插入一個鏈表時,就判斷這個新插入的鏈表是否有客戶端阻塞,有的話,就解除這個阻塞的客戶端,并且發送剛插入鏈表元素給客戶端,客戶端就這樣解除阻塞。

先看下有關數據結構,以及server和client有關屬性

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//阻塞狀態
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命令執行函數為

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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阻塞

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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函數只是簡單的設置客戶端屬性,如下

?
1
2
3
4
5
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調用中,執行以下語句

?
1
2
if (listLength(server.ready_keys))
   handleClientsBlockedOnLists();

這兩行代碼是先檢查server.ready_keys是否為空,如果不為空,說明已經有一些就緒的鏈表,此時可以判斷是否有客戶端阻塞在這個鍵值上,如果有,則喚醒;現在問題又來了,這個server.ready_keys在哪更新鏈表了?

原來是在dbAdd函數中,當往數據庫中添加的值類型為REDIS-LIST時,這時就要調用signalListAsReady函數將鏈表指針添加進server.ready_keys:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//db.c
void 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.c
void 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()函數來處理阻塞客戶端,在這個函數中,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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給客戶端回復剛在鏈表插入的元素。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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給客戶端回復一個空回復,以及接觸客戶端阻塞。

總結

鏈表消息隊列實現暫時分析到這了,大家都學會了嗎?希望這篇文章給大家能帶來一定的幫助,如果有疑問可以留言交流。

延伸 · 閱讀

精彩推薦
  • RedisRedis的配置、啟動、操作和關閉方法

    Redis的配置、啟動、操作和關閉方法

    今天小編就為大家分享一篇Redis的配置、啟動、操作和關閉方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧 ...

    大道化簡5312019-11-14
  • Redis詳解Redis復制原理

    詳解Redis復制原理

    與大多數db一樣,Redis也提供了復制機制,以滿足故障恢復和負載均衡等需求。復制也是Redis高可用的基礎,哨兵和集群都是建立在復制基礎上實現高可用的...

    李留廣10222021-08-09
  • RedisRedis 事務知識點相關總結

    Redis 事務知識點相關總結

    這篇文章主要介紹了Redis 事務相關總結,幫助大家更好的理解和學習使用Redis,感興趣的朋友可以了解下...

    AsiaYe8232021-07-28
  • RedisRedis全量復制與部分復制示例詳解

    Redis全量復制與部分復制示例詳解

    這篇文章主要給大家介紹了關于Redis全量復制與部分復制的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Redis爬蟲具有一定的參考學習...

    豆子先生5052019-11-27
  • Redisredis中如何使用lua腳本讓你的靈活性提高5個逼格詳解

    redis中如何使用lua腳本讓你的靈活性提高5個逼格詳解

    這篇文章主要給大家介紹了關于redis中如何使用lua腳本讓你的靈活性提高5個逼格的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具...

    一線碼農5812019-11-18
  • Redisredis 交集、并集、差集的具體使用

    redis 交集、并集、差集的具體使用

    這篇文章主要介紹了redis 交集、并集、差集的具體使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友...

    xiaojin21cen10152021-07-27
  • RedisRedis如何實現數據庫讀寫分離詳解

    Redis如何實現數據庫讀寫分離詳解

    Redis的主從架構,能幫助我們實現讀多,寫少的情況,下面這篇文章主要給大家介紹了關于Redis如何實現數據庫讀寫分離的相關資料,文中通過示例代碼介紹...

    羅兵漂流記6092019-11-11
  • Redisredis實現排行榜功能

    redis實現排行榜功能

    排行榜在很多地方都能使用到,redis的zset可以很方便地用來實現排行榜功能,本文就來簡單的介紹一下如何使用,具有一定的參考價值,感興趣的小伙伴們...

    乘月歸5022021-08-05
主站蜘蛛池模板: 49vv看片免费 | 欧美成人高清视频 | 国产一区日韩在线 | 一级片观看 | 欧美精品成人 | 国产欧美综合一区二区三区 | 日本午夜视频 | 国产成人jvid在线播放 | 中文字幕视频一区 | 欧美成人精品一区二区三区 | 精品国产一区二区三区免费 | 日韩av在线中文字幕 | 天天天干天天天操 | 午夜网址 | 日本福利视频 | 国产精久久久 | 麻豆av一区 | 日本精品久久 | 性福视频在线观看 | 一级a性色生活片久久毛片 国产精品久久久久久久久久免费看 | www国产网站| 国产日韩一区二区三区 | 日本一区二区免费播放 | 在线观看欧美一区 | 午夜精品一区 | 久久av一区| 国产3区| 五月天一区二区 | 日本淫片| 国内成人免费视频 | 欧美大片免费观看 | 在线观看午夜免费视频 | 日韩一二三区视频 | 午夜成人免费视频 | 国产成人久久一区二区三区 | 精品久久国产老人久久综合 | 中文字幕电影在线 | 日韩av在线影院 | 淫片一级国产 | 国产欧美高清在线观看 | 欧美日韩第一页 |