標籤:
參考:《Redis入門指南》第4章進階
http://book.51cto.com/art/201305/395461.htm
4.4.2 使用Redis實現任務隊列
說到隊列很自然就能想到Redis的清單類型,3.4.2節介紹了使用LPUSH和RPOP命令實現隊列的概念。如果要實現任務隊列,只需要讓生產者將任務使用LPUSH命令加入到某個鍵中,另一邊讓消費者不斷地使用RPOP命令從該鍵中取出任務即可。
在小白的例子中,完成發郵件的任務需要知道收件地址、郵件主題和郵件內文。所以生產者需要將這三個資訊組成對象並序列化成字串,然後將其加入到任務隊列中。而消費者則迴圈從隊列中拉取任務,就像如下虛擬碼:
- # 無限迴圈讀取任務隊列中的內容
- loop
- $task = RPOR queue
- if $task
- # 如果任務隊列中有任務則執行它
- execute($task)
- else
- # 如果沒有則等待1秒以免過於頻繁地請求資料
- wait 1 second
到此一個使用Redis實現的簡單的任務隊列就寫好了。不過還有一點不完美的地方:當任務隊列中沒有任務時消費者每秒都會調用一次RPOP命令查看是否有新任務。如果可以實現一旦有新任務加入任務隊列就通知消費者就好了。其實藉助BRPOP命令就可以實現這樣的需求。
BRPOP命令和RPOP命令相似,唯一的區別是當列表中沒有元素時BRPOP命令會一直阻塞住串連,直到有新元素加入。如上段代碼可改寫為:
- loop
- # 如果任務隊列中沒有新任務,BRPOP命令會一直阻塞,不會執行execute()。
- $task = BRPOP queue, 0
- # 傳回值是一個數組(見下介紹),數組第二個元素是我們需要的任務。
- execute($task[1])
BRPOP命令接收兩個參數,第一個是鍵名,第二個是逾時時間,單位是秒。當超過了此時間仍然沒有獲得新元素的話就會返回nil。上例中逾時時間為"0",表示不限制等待的時間,即如果沒有新元素加入列表就會永遠阻塞下去。
當獲得一個元素後BRPOP命令返回兩個值,分別是鍵名和元素值。為了測試BRPOP命令,我們可以開啟兩個redis-cli執行個體,在執行個體A中:
- redis A> BRPOP queue 0
brpop:
傳回值:
假如在指定時間內沒有任何元素被彈出,則返回一個 nil 和等待時間長度。反之,返回一個含有兩個元素的列表,第一個元素是被彈出元素所屬的 key ,第二個元素是被彈出元素的值。
鍵入斷行符號後執行個體1會處於阻塞狀態,這時在執行個體B中向queue中加入一個元素:
- redis B> LPUSH queue task
- (integer) 1
在LPUSH命令執行後執行個體A馬上就返回了結果:
- 1) "queue"
- 2) "task"
同時會發現queue中的元素已經被取走:
- redis> LLEN queue
- (integer) 0
除了BRPOP命令外,Redis還提供了BLPOP,和BRPOP的區別在與從隊列取元素時BLPOP會從隊列左邊取。具體可以參照LPOP理解,這裡不再贅述。
4.4.3 優先順序隊列
前面說到了小白部落格需要在發布文章的時候向每個訂閱者發送郵件,這一步驟同樣可以使用任務隊列實現。由於要執行的任務和發送確認郵件一樣,所以二者可以共用一個消費者。然而設想這樣的情況:假設訂閱小白部落格的使用者有1000人,那麼當發布一篇新文章後部落格就會向任務隊列中添加1000個發送通知訊息的任務。如果每發一封郵件需要10秒,全部完成這1000個任務就需要近3個小時。問題來了,假如這期間有新的使用者想要訂閱小白部落格,當他提交完自己的郵箱並看到網頁提示他查收確認郵件時,他並不知道向自己發送確認郵件的任務被加入到了已經有1000個任務的隊列中。要收到確認郵件,他不得不等待近3個小時。多麼糟糕的使用者體驗!而另一方面發布新文章後通知訂閱使用者的任務並不是很緊急,大多數使用者並不要求有新文章後馬上就能收到通知訊息,甚至延遲一天的時間在很多情況下也是可以接受的。
所以可以得出結論當發送確認郵件和發送通知訊息兩種任務同時存在時,應該優先執行前者。為了實現這一目的,我們需要實現一個優先順序隊列。
BRPOP命令可以同時接收多個鍵,其完整的命令格式為BRPOP key [key …] timeout,
按參數 key 的先後順序依次檢查各個列表,彈出第一個非空列表的尾部元素
如BRPOP queue:1 queue:2 0。意義是同時檢測多個鍵,如果所有鍵都沒有元素則阻塞,如果其中有一個鍵有元素則會從該鍵中彈出元素。例如,開啟兩個redis-cli執行個體,在執行個體A中:
- redis A> BRPOP queue:1 queue:2 queue:3 0
在執行個體B中:
- redis B> LPUSH queue:2 task
- (integer) 1
則執行個體A中會返回:
- 1) "queue:2"
- 2) "task"
如果多個鍵都有元素則按照從左至右的順序取第一個鍵中的一個元素。我們先在queue:2和queue:3中各加入一個元素:
- redis> LPUSH queue:2 task1
- 1) (integer) 1
- redis> LPUSH queue:3 task2
- 2) (integer) 1
然後執行BRPOP命令:
- redis> BRPOP queue:1 queue:2 queue:3 0
- 1) "queue:2"
- 2) "task1"
藉此特性可以實現區分優先順序的任務隊列。我們分別使用queue:confirmation. email和queue:notification.email兩個鍵儲存發送確認郵件和發送通知訊息兩種任務,然後將消費者的代碼改為:
- loop
- $task =
- BRPOP queue:confirmation.email,
- queue:notification.email,
- 0
- execute($task[1])
這時一旦發送確認郵件的任務被加入到queue:confirmation.email隊列中,無論queue: notification.email還有多少任務,消費者都會優先完成發送確認郵件的任務。
參考:http://book.51cto.com/art/201305/395463.htm
官網說法:
RPOPLPUSH source destination
命令 RPOPLPUSH 在一個原子時間內,執行以下兩個動作:
- 將列表 source 中的最後一個元素(尾元素)彈出,並返回給用戶端。
- 將 source 彈出的元素插入到列表 destination ,作為 destination 列表的的頭元素。
模式: 安全的隊列
Redis的列表經常被用作隊列(queue),用於在不同程式之間有序地交換訊息(message)。一個用戶端通過 LPUSH 命令將訊息放入隊列中,而另一個用戶端通過 RPOP 或者 BRPOP 命令取出隊列中等待時間最長的訊息。
不幸的是,上面的隊列方法是『不安全』的,因為在這個過程中,一個用戶端可能在取出一個訊息之後崩潰,而未處理完的訊息也就因此丟失。
使用 RPOPLPUSH 命令(或者它的阻塞版本 BRPOPLPUSH )可以解決這個問題:因為它不僅返回一個訊息,同時還將這個訊息添加到另一個備份列表當中,如果一切正常的話,當一個用戶端完成某個訊息的處理之後,可以用 LREM 命令將這個訊息從備份表刪除。
最後,還可以添加一個用戶端專門用於監視備份表,它自動地將超過一定處理時限的訊息重新放入隊列中去(負責處理該訊息的用戶端可能已經崩潰),這樣就不會丟失任何訊息了。
模式:迴圈列表 Circular list
通過使用相同的 key 作為 RPOPLPUSH 命令的兩個參數,用戶端可以用一個接一個地擷取列表元素的方式,取得列表的所有元素,而不必像 LRANGE 命令那樣一下子將所有列表元素都從伺服器傳送到用戶端中(兩種方式的總複雜度都是 O(N))。
以上的模式甚至在以下的兩個情況下也能正常工作:
- 有多個用戶端同時對同一個列表進行旋轉(rotating),它們擷取不同的元素,直到所有元素都被讀取完,之後又從頭開始。
- 有用戶端在向列表尾部(右邊)添加新元素。
這個模式使得我們可以很容易實現這樣一類系統:有 N 個用戶端,需要連續不斷地對一些元素進行處理,而且處理的過程必須儘可能地快。一個典型的例子就是伺服器的監控程式:它們需要在儘可能短的時間內,並行地檢查一組網站,確保它們的可訪問性。
注意,使用這個模式的用戶端是易於擴充(scala)且安全(reliable)的,因為就算接收到元素的用戶端失敗,元素還是儲存在列表裡面,不會丟失,等到下個迭代來臨的時候,別的用戶端又可以繼續處理這些元素了。
更多:
http://redis.io/commands/blpop
\http://redis.io/commands/rpoplpush
redis實現隊列queue