標籤:
在網站開發中,當頁面需要進行如發送郵件、複雜資料運算等耗時較長的操作時會阻塞頁面的渲染。為了避免使用者等待太久,應該使用獨立的線程來完成這類操作。 不過一些程式設計語言或架構不易實現多線程,這時很容易就會想到通過其他進程來實現。設想有一個進程能夠完成發郵件的功能,那麼在頁面中只需要想辦法通知這個 進程向指定的地址發送郵件就可以了。
通知的過程可以藉助任務隊列來實現。任務隊列顧名思義,就是“傳遞任務的隊列”。與任務隊列進行互動的實體有兩類,一類是生產者(producer),一類是消費者(consumer)。生產者會將需要處理的任務放入任務隊列中,而消費者則不斷地從任務隊列中讀入任務資訊並執行。
對於發郵件這個操作來說頁面程式就是生產者,而發郵件的進程就是消費者。當需要發送郵件時,頁面程式會將收件地址、郵件主題和郵件內文組裝成一個任務後存入任務隊列中。同時發郵件的進程會不斷檢查任務隊列,一旦發現有新的任務便會將其從隊列中取出並執
行。由此實現了進程間的通訊。
使用任務隊列有如下好處:
(1)松耦合。生產者和消費者無需知道彼此的實現細節,只需要約定好任務的描述格式。這使得生產者和消費者可以由不同的團隊使用不同的程式設計語言編寫。
(2)易於擴充消費者可以有多個,而且可以分布在不同的伺服器中,藉此可以輕易地降低單台伺服器的負載。
下面我們使用Redis實現任務隊列
Redis的清單類型,使用LPUSH和RPOP命令實現隊列。如果要實現任務隊列,只需要讓生產者將任務使用LPUSH命令加入到某個鍵中,另一邊讓消費者不斷地使用RPOP命令從該鍵中取出任務即可。
在上面例子中,完成發郵件的任務需要知道收件地址、郵件主題和郵件內文。所以生產者需要將這三個資訊組成對象並序列化成字串,然後將其加入到任務隊列中。而消費者則迴圈從隊列中拉取任務,就像如下虛擬碼:
[html] view plaincopyprint?
- #無限迴圈讀取任務隊列中的內容
- loop
- $task=RPOR queue
- if $task
- #如果任務隊列中有任務則執行它
- execute($task)
- else
- #如果沒有則等待1秒以免過於頻繁地請求資料
- wait 1 second
到此一個使用Redis實現的簡單的任務隊列就寫好了。不過還有一點不完美的地方:當任務隊列中沒有任務時消費者每秒都會調用一次RPOP命令查看是否有新任務。如果可以實現一旦有新任務加入任務隊列就通知消費者就好了。其實藉助 BRPOP 命令就可以實現這樣的需求。
BRPOP命令和RPOP命令相似,唯一的區別是當列表中沒有元素時BRPOP命令會一直阻塞住串連,直到有新元素加入。如上段代碼可改寫為:
[html] view plaincopyprint?
- loop
- #如果任務隊列中沒有新任務,BRPOP命令會一直阻塞,不會執行execute()。
- $task=BRPOP queue, 0
- #傳回值是一個數組(見下介紹),數組第二個元素是我們需要的任務。
- execute($task[1])
BRPOP命令接收兩個參數,第一個是鍵名,第二個是逾時時間,單位是秒。當超過了此時間仍然沒有獲得新元素的話就會返回nil。上例中逾時時間為“0”,表示不限制等待的時間,即如果沒有新元素加入列表就會永遠阻塞下去。
當獲得一個元素後BRPOP命令返回兩個值,分別是鍵名和元素值。為了測試BRPOP命令,我們可以開啟兩個redis-cli執行個體,在執行個體A中:
[html] view plaincopyprint?
- redis A>BRPOP queue 0
鍵入斷行符號後執行個體1會處於阻塞狀態,這時在執行個體B中向queue中加入一個元素:
[html] view plaincopyprint?
- redis B>LPUSH queue task
- (integer) 1
在LPUSH命令執行後執行個體A馬上就返回了結果:
[html] view plaincopyprint?
- 1) "queue"
- 2) "task"
同時會發現queue中的元素已經被取走:
[html] view plaincopyprint?
- redis>LLEN queue
- (integer) 0
除了BRPOP命令外,Redis還提供了BLPOP,和BRPOP的區別在與從隊列取元素時BLPOP會從隊列左邊取。
Redis優先順序隊列
當發送確認郵件和發送通知訊息(可以延遲)兩種任務同時存在時,應該優先執行前者。為了實現這一目的,我們需要實現一個優先順序隊列。
BRPOP命令可以同時接收多個鍵,其完整的命令格式為BLPOP key [key …]timeout ,如BLPOP queue:1 queue:2 0。意義是同時檢測多個鍵,如果所有鍵都沒有元素則阻塞,如果其中有一個鍵有元素則會從該鍵中彈出元素。例如,開啟兩個redis-cli執行個體,在執行個體A中:
[html] view plaincopyprint?
- redis A>BLPOP queue:1 queue:2 queue:3 0
在執行個體B中:
[html] view plaincopyprint?
- redis B>LPUSH queue:2 task
- (integer) 1
則執行個體A中會返回:
[html] view plaincopyprint?
- 1) "queue:2"
- 2) "task"
如果多個鍵都有元素則按照從左至右的順序取第一個鍵中的一個元素。我們先在queue:2和queue:3中各加入一個元素:
[html] view plaincopyprint?
- redis>LPUSH queue:2 task1
- 1) (integer) 1
- redis>LPUSH queue:3 task2
- 2) integer) 1
然後執行BRPOP命令:
[html] view plaincopyprint?
- redis>BRPOP queue:1 queue:2 queue:3 0
- 1) "queue:2"
- 2) "task1"
藉此特性可以實現區分優先順序的任務隊列。我們分別使用queue:confirmation.email和queue:notification.email兩個鍵儲存發送確認郵件和發送通知訊息兩種任務,然後將消費者的代碼改為:
[html] view plaincopyprint?
- loop
- $task =
- BRPOP queue:confirmation.email,
- queue:notification.email,
- 0
- execute(task[1])
- http://www.youyuanapp.com/thread-11419-1-1.html
http://www.youyuanapp.com/thread-11418-1-1.html
http://www.youyuanapp.com/thread-11417-1-1.html
http://www.youyuanapp.com/thread-11412-1-1.html
http://www.youyuanapp.com/thread-11409-1-1.html
http://www.youyuanapp.com/thread-11404-1-1.html
http://www.youyuanapp.com/thread-11403-1-1.html
http://www.youyuanapp.com/thread-11398-1-1.html
http://www.youyuanapp.com/thread-11397-1-1.html
http://www.youyuanapp.com/thread-11395-1-1.html
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101147963/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101251829/
http://yishujiayuanq.blog.163.com/blog/static/244725061201502510133740/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101653328/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101718995/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101738627/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101822599/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101841318/
http://yishujiayuanq.blog.163.com/blog/static/2447250612015025101927982/
http://yishujiayuanq.blog.163.com/blog/static/244725061201502510197287/
Redis研究(十五)—任務隊列