標籤:
為什麼需要訊息佇列
系統中引入訊息佇列機制是對系統一個非常大的改善。例如一個web系統中,使用者做了某項操作後需要發送郵件通知到使用者郵箱中。你可以使用同步方式讓使用者等待郵件發送完成後反饋給使用者,但是這樣可能會因為網路的不確定性造成使用者長時間的等待從而影響使用者體驗。
有些情境下是不可能使用同步方式等待完成的,那些需要後台花費大量時間的操作。例如極端例子,一個線上編譯系統任務,背景編譯完成需要30分鐘。這種情境的設計不可能同步等待後在回饋,必須是先反饋使用者隨後非同步處理完成,再等待處理完成後根據情況再此反饋使用者與否。
另外適用訊息佇列的情況是那些系統處理能力有限的情況下,先使用隊列機制把任務暫時存放起來,系統再一個個輪流處理掉排隊的任務。這樣在系統輸送量不足的情況下也能穩定的處理掉高並發的任務。
訊息佇列可以用來做排隊機制,只要系統需要用到排隊機制的地方就可以使用訊息佇列來作。
使用redis怎麼做訊息佇列
首先redis它的設計是用來做緩衝的,但是由於它自身的某種特性使得他可以用來做訊息佇列。它有幾個阻塞式的API可以使用,正是這些阻塞式的API讓他有做訊息佇列的能力。
redis能做訊息佇列得益於他list對象blpop brpop介面以及Pub/Sub(發布/訂閱)的某些介面。他們都是阻塞版的,所以可以用來做訊息佇列。
Redis實現先進先出隊列
Redis實現FIFO很容易,只需要一個List對象從頭取資料,從尾部塞資料即可實現。例如lpush存資料,brpop取資料。
Redis實現優先順序隊列
首先brpop和blpop是支援多list讀取的,比如brpop lista listb 0 命令就可以實現先從lista讀取資料,讀取完lista的資料再去讀取listb的資料。
那麼我們就可以通過如下方式實現了:
127.0.0.1:6379> lpush a 1(integer) 1127.0.0.1:6379> lpush a 2(integer) 2127.0.0.1:6379> lpush a 3(integer) 3127.0.0.1:6379> lpush b 1(integer) 1127.0.0.1:6379> lpush b 2(integer) 2127.0.0.1:6379> lpush b 3(integer) 3
127.0.0.1:6379> brpop a b 01) "a"2) "1"127.0.0.1:6379> brpop a b 01) "a"2) "2"127.0.0.1:6379> brpop a b 01) "a"2) "3"127.0.0.1:6379> brpop a b 01) "b"2) "1"127.0.0.1:6379> brpop a b 01) "b"2) "2"127.0.0.1:6379> brpop a b 01) "b"2) "3"127.0.0.1:6379> brpop a b 0
這種方案我們可以支援不同階段的優先順序隊列,例如高中低三個層級或者更多的層級都可以。
多優先順序問題解決
如果優先順序層級很多的情況,假設有個這樣的需求,優先順序不是簡單的高中低或者0-10這些固定的層級。而是類似0-99999這麼多層級。那麼我們第三種方案將不太合適了。
雖然redis有sorted set這樣的可以排序的資料類型,看是很可惜它沒有阻塞版的介面。於是我們還是只能使用list類型通過其他方式來完成目的。
有個簡單的做法我們可以只設定一個隊列,並保證它是按照優先順序排序號的。然後通過二分尋找法尋找一個任務合適的位置,並通過 lset 命令插入到相應的位置。
例如隊列裡麵包含著寫優先順序的任務[1, 3, 6, 8, 9, 14],當有個優先順序為7的任務過來,我們通過自己的二分演算法一個個從隊列裡面取資料出來反和目標資料比對,計算出相應的位置然後插入到指定地點即可。
因為二分尋找是比較快的,並且redis本身也都在記憶體中,理論上速度是可以保證的。但是如果說資料量確實很大的話我們也可以通過一些方式來調優。
把上面的方案結合起來就會很大程度上減少開銷。例如資料量十萬的隊列,它們的優先順序也是隨機0-十萬的區間。我們可以設定 10個或者100個不同的隊列,0-一萬的優先順序任務投放到1號隊列,一萬-二萬的任務投放到2號隊列。這樣將一個隊列按不同等級拆分後它單個隊列的資料 就減少許多,這樣二分尋找匹配的效率也會高一點。但是資料所佔的資源基本是不變的,十萬資料該佔多少記憶體還是多少。只是系統裡面多了一些隊列而已。
redis實現定時訊息佇列
由於Redis排序集合(Sorted Sets)沒有實現阻塞功能,所以只能通過程式自己實現。score欄位存入時間戳記,由於時間戳記較長我們用三位元字代替。
127.0.0.1:6379> zadd seta 100 a(integer) 1127.0.0.1:6379> zadd seta 200 b(integer) 1127.0.0.1:6379> zadd seta 300 c(integer) 1127.0.0.1:6379> zadd seta 300 d(integer) 1
首先我們插入4條資料。
然後我們擷取0到目前時間的資料。比如目前時間戳為200,那麼我們執行如下命令
127.0.0.1:6379> zrangebyscore seta 0 200 limit 0 11) "a"127.0.0.1:6379> zrem seta a(integer) 1127.0.0.1:6379> zrangebyscore seta 0 201 limit 0 11) "b"127.0.0.1:6379> zrem seta b(integer) 1127.0.0.1:6379> zrangebyscore seta 0 202 limit 0 1(empty list or set)
如果取到空資料,阻塞一段時間,然後繼續取資料,迴圈執行即可。
這裡我們為什麼沒有採用zremrangebyscore命令而是採用zrangebyscore和zrem組合,因為zremrangebyscore沒有limit參數,可能取到多行資料(例如兩個資料socore一樣等),由於並發問題可能導致zrem返回0,這樣也沒事,我們繼續取即可。
java程式碼片段:
public String getData() throws Exception { Jedis jedis = getResource(); while (true) { Set<String> seta = jedis.zrangeByScore("seta", 0, System.currentTimeMillis(), 0, 1); if (seta != null && seta.size() > 0) { String data = seta.toArray(new String[] {})[0]; Long res = jedis.zrem("seta", data); if (res > 0) { return data; } } Thread.sleep(1000L); }}
來自個人部落格:http://www.jflyfox.com/mtg/front/article/997.html
用redis實現訊息佇列