redis怎麼做消息隊列?

有個想法,用兩個伺服器,node處理請求,把數據全部push到 redis緩存隊列中,另一個php伺服器不斷的pop這個隊列里的數據然後與mysql交互做持久化。。。大家覺得這麼做怎麼樣 ?


如其它答案,基本有兩種解決方案
1. Redis自帶的PUB/SUB機制,即發布-訂閱模式。這種模式生產者(producer)和消費者(consumer)是1-M的關係,即一條消息會被多個消費者消費,當只有一個消費者時即可以看做一個1-1的消息隊列,但這種方式並不適合題主的場景。首先,數據可靠性的無法保障,題主的數據最終需要落庫,如果消息丟失、Redis宕機部分數據沒有持久化甚至突然的網路抖動都可能帶來數據的丟失,應該是無法忍受的。其次,擴展不靈活,沒法通過多加consumer來加快消費的進度,如果前端寫入數據太多,同步會比較慢,數據不同步的狀態越久,風險越大,當然可以通過channel拆分的方式來解決,雖然不靈活,但可以規避。這種方案更適合於對數據可靠性要求不高,比如一些統計日誌打點。

2. Redis的PUSH/POP機制,利用的Redis的列表(lists)數據結構。比較好的使用模式是,生產者lpush消息,消費者brpop消息,並設定超時時間,可以減少redis的壓力。這種方案相對於第一種方案是數據可靠性提高了,只有在Redis宕機且數據沒有持久化的情況下丟失數據,可以根據業務通過AOF和縮短持久化間隔來保證很高的可靠性,而且也可以通過多個client來提高消費速度。但相對於專業的消息隊列來說,該方案消息的狀態過於簡單(沒有狀態),且沒有ack機制,消息取出後消費失敗依賴於client記錄日誌或者重新push到隊列裡面。

最後再來看題主的需求,是希望先寫Redis,再非同步同步到mysql裡面,期望數據的最終一致性。這樣帶來的好處是前端寫的請求飛速啊(不用落盤當然快),問題是很複雜,而且不太合理。假設是合理的話,就應該選擇一個更可靠的消息中間件,比如Redis作者開源的Disque,或者阿里開源RocketMQ,以及基於Golang的nsq等,Redis更適合用來存數據。

為什麼說題主的需求不合理?
類似於這種先寫緩存再同步到DB的,目的是為了減少DB壓力,提升前端API性能。題主的方案雖然能做到這兩點,但忽略了根本的一點:數據不管存到哪裡,都是用來訪問(使用)的。但在題主的方案里,寫入Redis的數據除了同步到DB里,不接任何訪問量,並沒有什麼卵用,最後DB讀的壓力上來了,還得把數據重新LOAD回Redis裡面,得不償失。

什麼樣的架構更合理?
只簡單說一下。
非同步寫入,在百度、58同城使用很多,基本的架構是先抽象出對象訪問層(或者只是緩存層),對外屏蔽數據來源(Redis、mysql、others)。對象訪問層對於某類數據的格式是定義好的,寫的請求來了直接寫入緩存(Redis),這樣前端的讀請求就直接讀緩存了,這樣存入的數據就有意義(當然了,接了大大的讀的量啊)。然後是數據落地(最終一致性問題),簡單的可以讀RDB文件(實時性不高),複雜一點的可以實現Redis的主從同步協議(實時性高於前一種)。第一種簡單,效率低,第二種複雜,效率高。
從Redis談起(三)—— Redis和消息中間件 這邊文章比較詳細得講解了Redis和消息中間件

謝謝,希望對大家有幫助。


lpush rpop


redis本身就支持pub/sub模式,可以作為消息隊列處理,參見 Command reference

按題主的場景,需要配置redis的持久化保存,否則可能會出現數據沒有保存到mysql中的情形。


redis做消息隊列,一旦有業務爆發,內存不夠直接掛掉,對伺服器內存要求多。建議用ssdb,和redis兼容,解決Redis容量有限問題。


lpush和lpop適合樓主


lpush queue
brpop queue 0
brpop還能實現優先順序隊列,詳情參考《Redis入門指南》第四章。


原文鏈接 :Redis設計思路學習與總結 - 騰雲閣

全文:

宋增寬,騰訊工程師,16年畢業加入騰訊,從事海量服務後台設計與研發工作,現在負責QQ群後台等項目,喜歡研究技術,並思考技術演變,專註於高並發業務架構的設計與性能優化。

下半年利用空餘時間研究和分析了部分Redis源碼,本文從網路模型、數據結構和內存管理、持久化和多機協作四個角度對redis的設計思路進行了分析,若有不正確之處,希望各路大神指出。

Redis是業界普遍應用的緩存組件,研究一個組件框架,最直觀的辦法就是從應用方的角度出發,將每個步驟的考慮一番,從這些步驟入手去研究往往能夠最快的體會到一個組件框架的設計哲學。以Redis為例,每當發起一條請求時,redis是如何管理管理網路請求,收到請求後又是通過什麼樣的數據結構進行組織並操作內存,這些數據又是如何dump到磁碟實現持久化,再到多機環境下如何同步和保證一致性……本文就是從網路模型、數據結構設計與內存管理、持久化方法和多機四個角度簡要描述了redis的設計和自己的一點體會。

一.網路模型

Redis是典型的基於Reactor的事件驅動模型,單進程單線程,高效的框架總是類似的。網路模型與spp的非同步模型幾乎一致。

Redis流程上整體分為接受請求處理器、響應處理器和應答處理器三個同步模塊,每一個請求都是要經歷這三個部分。

Redis集成了libevent/epoll/kqueue/select等多種事件管理機制,可以根據操作系統版本自由選擇合適的管理機制,其中libevent是最優選擇的機制。

Redis的網路模型有著所有事件驅動模型的優點,高效低耗。但是面對耗時較長的操作的時候,同樣無法處理請求,只能等到事件處理完畢才能響應,之前在業務中也遇到過這樣的場景,刪除redis中全量的key-value,整個操作時間較長,操作期間所有的請求都無法響應。所以了解清楚網路模型有助於在業務中揚長避短,減少長耗時的請求,儘可能多一些簡單的短耗時請求發揮非同步模型的最大的威力,事實上在Redis的設計中也多次體現這一點。

二.數據結構和內存管理

1.字元串

1.1 結構

Redis的字元串是對C語言原始字元串的二次封裝,結構如下:

struct sdshdr {
long len;
long free;
char buf[];
};

可以看出,每當定義一個字元串時,除了保存字元的空間,Redis還分配了額外的空間用於管理屬性欄位。

1.2 內存管理方式

動態內存管理方式,動態方式最大的好處就是能夠較為充分的利用內存空間,減少內存碎片化,與此同時帶來的劣勢就是容易引起頻繁的內存抖動,通常採用「空間預分配」和「惰性空間釋放」兩種優化策略來減少內存抖動,redis也不例外。

每次修改字元串內容時,首先檢查內存空間是否符合要求,否則就擴大2倍或者按M增長;減少字元串內容時,內存並不會立刻回收,而是按需回收。

關於內存管理的優化,最基本的出發點就是浪費一點空間還是犧牲一些時間的權衡,像STL、tcmalloc、protobuf3的arena機制等採用的核心思路都是「預分配遲回收」,Redis也是一樣的。

1.3 二進位安全

判斷字元串結束與否的標識是len欄位,而不是C語言的"",因此是二進位安全的。
放心的將pb序列化後的二進位字元串存入redis。
簡而言之,通過redis的簡單封裝,redis的字元串的操作更加方便,性能更友好,並且屏蔽了C語言字元串的一些需要用戶關心的問題。

2.字典(哈希)

字典的底層一定是hash,涉及到hash一定會涉及到hash演算法、衝突的解決方法和hash表擴容和縮容。

2.1 hash演算法

Redis使用的就是常用的Murmurhash2,Murmurhash演算法能夠給出在任意輸入序列下的散列分布性,並且計算速度很快。之前做共享內存的Local-Cache的需求時也正是利用了Murmurhash的優勢,解決了原有結構的hash函數散列分布性差的問題。

2.2 hash衝突解決方法

鏈地址法解決hash衝突,通用解決方案沒什麼特殊的。多說一句,如果選用鏈地址解決衝突,那麼勢必要有一個散列性非常好的hash函數,否則hash的性能將會大大折扣。Redis選用了Murmurhash,所以可以放心大膽的採用鏈地址方案。

2.3 hash擴容和縮容

維持hash表在一個合理的負載範圍之內,簡稱為rehash過程。
rehash的過程也是一個權衡的過程,在做評估之前首先明確一點,不管中間採用什麼樣的rehash策略,rehash在宏觀上看一定是:分配一個新的內存塊,老數據搬到新的內存塊上,釋放舊內存塊。
老數據何時搬?怎麼搬?就變成了一個需要權衡的問題。
第一部分的網路模型上明確的指出Redis的事件驅動模型特點,不適合玩長耗時操作。如果一個hashtable非常大,需要進行擴容就一次性把老數據copy過去,那就會非常耗時,違背事件驅動的特點。所以Redis依舊採用了一種惰性的方案:
新空間分配完畢後,啟動rehashidx標識符表明rehash過程的開始;之後所有增刪改查涉及的操作時都會將數據遷移到新空間,直到老空間數據大小為0表明數據已經全部在新空間,將rehashidx禁用,表明rehash結束。
將一次性的集中問題分而治之,在Redis的設計哲學中體現的淋漓盡致,主要是為了避免大耗時操作,影響Redis響應客戶請求。

3.整數集合

變長整數存儲,整數分為16/32/64三個變長尺度,根據存入的數據所屬的類型,進行規劃。
每次插入新元素都有可能導致尺度升級(例如由16位漲到32位),因此插入整數的時間複雜度為O(n)。這裡也是一個權衡,內存空間和時間的一個折中,儘可能節省內存。

4.跳躍表

Redis的skilplist和普通的skiplist沒什麼不同,都是冗餘數據實現的從粗到細的多層次鏈表,Redis中應用跳錶的地方不多,常見的就是有序集合。
Redis的跳錶和普通skiplist沒有什麼特殊之處。

5.鏈表

Redis的鏈表是雙向非循環鏈表,擁有表頭和表尾指針,對於首尾的操作時間複雜度是O(1),查找時間複雜度O(n),插入時間複雜度O(1)。
Redis的鏈表和普通鏈表沒有什麼特殊之處。

三.AOF和RDB持久化

AOF持久化日誌,RDB持久化實體數據,AOF優先順序大於RDB。

1.AOF持久化

機制:通過定時事件將aof緩衝區內的數據定時寫到磁碟上。

2.AOF重寫

為了減少AOF大小,Redis提供了AOF重寫功能,這個重寫功能做的工作就是創建一個新AOF文件代替老的AOF,並且這個新的AOF文件沒有一條冗餘指令。(例如對list先插入A/B/C,後刪除B/C,再插入D共6條指令,最終狀態為A/D,只需1條指令就可以)
實現原理就是讀現有資料庫的狀態,根據狀態反推指令,跟之前的AOF無關。同樣,為了避免長時間耗時,重寫工作放在子進程進行。

3.RDB持久化

SAVE和BGSAVE兩個命令都是用於生成RDB文件,區別在於BGSAVE會fork出一個子進程單獨進行,不影響Redis處理正常請求。
定時和定次數後進行持久化操作。
簡而言之,RDB的過程其實是比較簡單的,滿足條件後直接去寫RDB文件就結束了。

四.多機和集群

1.主從伺服器

避免單點是所有服務的通用問題,Redis也不例外。解決單點就要有備機,有備機就要解決固有的數據同步問題。

1.1 sync——原始版主從同步

Redis最初的同步做法是sync指令,通過sync每次都會全量數據,顯然每次都全量複製的設計比較消耗資源。改進思路也是常規邏輯,第一次全量,剩下的增量,這就是現在的psync指令的活。

1.2 psync

部分重同步實現的技術手段是「偏移序號+積壓緩衝區」,具體做法如下:
(1)主從分別維護一個seq,主每次完成一個請求便seq+1,從每同步完後更新自己seq;
(2)從每次打算同步時都是攜帶著自己的seq到主,主將自身的seq與從做差結果與積壓緩衝區大小比較,如果小於積壓緩衝區大小,直接從積壓緩衝區取相應的操作進行部分重同步;
(3)否則說明積壓緩衝區不能夠cover掉主從不一致的數據,進行全量同步。
本質做法用空間換時間,顯然在這裡犧牲部分空間換回高效的部分重同步,收益比很大。

2.Sentinel

本質:多主從伺服器的Redis系統,多台主從上加了管理監控,以保證系統高可用性。

3.集群

Redis的官方版集群尚未在工業界普及起來,下面主要介紹一下集群的管理體系和運轉體系。

2.1 slot-集群單位

集群的數據區由slot組成,每個節點負責的slot是在集群啟動時分配的。

2.2 客戶請求

客戶請求時如果相應數據hash後不屬於請求節點所管理的slots,會給客戶返回MOVED錯誤,並給出正確的slots。
從這個層面看,redis的集群還不夠友好,集群內部的狀態必須由客戶感知。

2.3 容災

主從伺服器,從用於備份主,一旦主故障,從代替主。

通過Redis的研究,深刻體會到的一點就是:所有設計的過程都是權衡和割捨的過程。同樣放到日常的工作和開發中也是如此,一句代碼寫的好不好,一個模塊設計的是否科學,就從速度和內存的角度去衡量看是否需要優化,並去評估每一種優化會收益到什麼,同時會損失什麼,收益遠大於損失的就是好的優化,這樣往往對於開發和提升更有針對性,更能提高效率。


redis 的 list結構,可滿足你最基本的 push/pop

然而 比較複雜一點的需求,比如插隊、批量獲取、查找 等就無法實現了,不過可以使用 zset()有序集合)加一些代碼邏輯實現。

畢竟zset 沒有pop ,它只能 獲取一個元素+刪除該元素 兩步操作,並發下很難保證原子性。

具體做法可參考一個簡單例子:https://github.com/liukelin/php_task_queue
它裡面的關於redis隊列消費方法


Redis 消息隊列介紹 查看詳情:Redis 消息隊列介紹-博客-雲棲社區-阿里雲

Redis的消息隊列使用簡單,沒有什麼配置,比ActiveMQ要輕量級太多,當然功能也比較簡單,如果只需要簡單的訂閱以及發布,可以考慮使用它。

訂閱操作

命令為:subscribe [channel] [channel] ..,如【代碼1】所示,即成功訂閱頻道[redis.blog]。

發布操作

命令為publish [channel] [message],如【代碼2】所示,【圖1】為訂閱的客戶端展示效果。
【代碼1】:

subscribe "redis. blog"

【代碼2】

publish "redis.blog" "hello redis"

【圖1】

退訂操作

命令為:unsubscribe [channel] [channel] ..,如【代碼3】所示。
【代碼3】

unsubscribe "redis.blog"

模式訂閱

命令為:psubscribe [pattern] [pattern]...,如【代碼4】所示,如果發布的消息符合當前訂閱的模式,亦會收到消息通知,如【圖2】所示。
【代碼4】

psubscribe "redis.*"

【圖2】

模式退訂
命令為:punsubscribe [pattern] [pattern]...,如【代碼5】所示。
【代碼5】

punsubscribe "redis.*"

數據結構
關於頻道以及模式,redis通過兩個struct實現,在redisServer中是如下定義的:

struct rediServer{
dict *pubsub_channels;
list *pubsub_patterns;
}

頻道是一個dict,pubsub_channels中key為頻道名稱,value為一個list,list中存儲了所有訂閱當前頻道的client機器;
訂閱:如果當前頻道還不存在,則subscribe操作即為一次dictAdd操作,如果存在,則相當於將當前的client append到當前channel對應的value list中;
退訂:從dict中get(channel)獲得list,從list中刪除當前client,如果刪除後list為空,則表示當前頻道已經沒有訂閱者了,此時將會刪除當前channel。

模式是一個list,list中每個node為一個pubsubPattern結構,定義如下:

typedef struct pubsubPattern{
redisClient *client;
robj *pattern;
}

訂閱模式:即是在當前list隊尾插入一個pubsubPattern;
退訂模式:則是遍歷list刪除匹配節點的過程。
即使不同client訂閱同一個模式,也是兩個不同的node,或者同一個client訂閱2個不同模式,亦為兩個node,這一點從pubsubPattern的數據結構上能看出來。

發送消息
發布一條信息,redis伺服器會執行2個操作:

  • 將消息發送給相應頻道的所有訂閱者,具體操作為: 以當前頻道為key,在當前pubsub_channels字典中找到對應的value,value為一個訂閱者list,遍歷該list,將消息發送給所有的訂閱者;
  • 將消息發送給與當前頻道相匹配的所有模式,具體操作為: 以當前頻道為key,在當前pubsub_patterns列表中遍歷所有節點,如果某個節點(pubsubPattern)的pattern值和key匹配,則將消息發送給當前節點的client,遍歷結束為止。

另外的三個命令

  • pubsub channels:查看當前所有頻道;也可以使用通配,返回所有匹配的平道(pubsub channels 「redis.*");
  • pubsub numsub: 接收任意多個頻道作為輸入參數,返回這些頻道的訂閱者數量;
  • pubsub numpat:返回伺服器當前被訂閱模式的數量;

至於如何將發布的消息及時反饋給所有訂閱者,redis是通過伺服器的文件事件來操作的,不單單是消息隊列功能,所有的get,set操作都是通過文件事件(file event)來驅動的。


用redis的list, 先lpush, 然後一個線程去rpop數據處理。


pubsub解決的是廣播問題,poppush才是解決隊列問題。


這種問題關鍵還是看你的數據可靠性要求如何,如果不能容忍丟失,那還是上kafka吧,至少多備份,持久化。另外一個問題,生產者生產的速度以及消費者消費的速度也決定了隊列選型。另外,redis 作者自己說過,redis並不是被設計用來作隊列的,強烈推薦kafka。


千萬別用redis來做消息隊列。

別問我怎麼知道的。


使用redis的問題在於,無法做事務處理,比如你pop出來之後,讀的進程掛了(雖然概率不大),還沒來得及寫入資料庫,這時就存在丟數據的風險,如果數據不重要,那是沒關係,如果是對一致性要求高,可以考慮rabbitmq,但是效率低一些,或者阿里雲的mns,要收費就是了


樓主場景用redis的話更適合lpush/rpop,而不是pub/sub


和我畢業設計做的爬蟲一樣,一個進程持續往redis里rpush數據,另一個進程lpop


參照: 使用NODEJS+REDIS開發一個消息隊列以及定時任務處理


https://github.com/kr/beanstalkd/wiki/client-libraries


之前嘗試過用redis作pub/sub 但是由於數據有可靠性需求,所以最終使用了beanstalkd作為redis的替代品,除了更加多樣的控制消息的生命週期以外,還可以在不可靠的環境下,可以保證丟失消息的backup找回


redis自帶管道形式的pub/sub


推薦閱讀:

Mozilla Rhino 和 Node.js 兼容嗎?

TAG:Web開發 | 分散式計算 | PHP | Redis | Nodejs |