Redis編程實踐【pub/sub】
部落格分類: Redis
Redis或許已經在很多企業開始推廣並試水,本文也根據個人的實踐,簡單描述一下Redis在實際開發過程中的使用(部署與架構,稍後介紹),程式執行環境為java + jedis,關於spring下如何整合redis-api,稍後介紹吧。
前言:下載redis-2.6.2,安裝好redis之後,請在redis.conf檔案中,將如下3個配置屬性開啟(僅供測試使用): Xml代碼 ##用戶端連結的連接埠,也是server端偵聽client連結的連接埠 ##每個client執行個體,都將和server在此連接埠上建立tcp長連結 port 6379 ## server端綁定的ip地址,如果一個物理機器有多個網路介面時,可以明確指定為某個網口的ip地址 bind 127.0.0.1 ##連結中io操作空閑時間,如果在指定時間內,沒有IO操作,連結將會被關閉 ##此屬性和TCP連結中的timeout選項一樣,建議設定為0,很多時候,我們一個應用也只會有一個redis執行個體 ##不過,如果你使用串連池的話,你需要對此參數做額外的考慮。 timeout 0
##用戶端連結的連接埠,也是server端偵聽client連結的連接埠 ##每個client執行個體,都將和server在此連接埠上建立tcp長連結 port 6379 ## server端綁定的ip地址,如果一個物理機器有多個網路介面時,可以明確指定為某個網口的ip地址 bind 127.0.0.1 ##連結中io操作空閑時間,如果在指定時間內,沒有IO操作,連結將會被關閉 ##此屬性和TCP連結中的timeout選項一樣,建議設定為0,很多時候,我們一個應用也只會有一個redis執行個體 ##不過,如果你使用串連池的話,你需要對此參數做額外的考慮。 timeout 0
Pub/Sub: "發布/訂閱",對於此功能,我們將會想到很多JMS實現,Redis提供此功能顯的“多此一舉”;不過這個功能在redis中,被設計的非常輕量級和簡潔,它做到了訊息的“發布”和“訂閱”的基本能力,但是尚未提供JMS中關於訊息的持久化/耐久性等各種企業級的特性。
一個Redis client發布訊息,其他多個redis client訂閱訊息,發布的訊息“即發即失”,redis不會持久儲存發布的訊息;訊息訂閱者也將只能得到訂閱之後的訊息,通道中此前的訊息將無從獲得。這就類似於JMS中“非持久”類型的訊息。
訊息發行者,即publish用戶端,無需獨佔連結,你可以在publish訊息的同時,使用同一個redis-client連結進行其他動作(例如:INCR等)
訊息訂閱者,即subscribe用戶端,需要獨佔連結,即進行subscribe期間,redis-client無法穿插其他動作,此時client以阻塞的方式等待“publish端”的訊息;這一點很好理解,因此subscribe端需要使用單獨的連結,甚至需要在額外的線程中使用。
一旦subscribe端取消連結,將會失去部分訊息,如果你非常關注每個訊息,那麼你應該考慮使用JMS或者基於Redis做一些額外的補充工作,如果你期望訂閱是持久的,那麼如下的設計思路可以借鑒(如下原理基於JMS):
1) subscribe端首先向一個Set集合中增加“訂閱者ID”,此Set集合儲存了“活躍訂閱”者,訂閱者ID標記每個唯一的訂閱者,例如:sub:email,sub:web。此SET稱為“活躍訂閱者集合”
2) subcribe端開啟訂閱操作,並基於Redis建立一個以“訂閱者ID”為KEY的LIST資料結構,此LIST中儲存了所有的尚未消費的訊息。此LIST稱為“訂閱者訊息佇列”
3) publish端:每發布一條訊息之後,publish端都需要遍曆“活躍訂閱者集合”,並依次向每個“訂閱者訊息佇列”尾部追加此次發布的訊息。
4) 到此為止,我們可以基本保證,發布的每一條訊息,都會持久儲存在每個“訂閱者訊息佇列”中。
5) subscribe端,每收到一個訂閱訊息,在消費之後,必須刪除自己的“訂閱者訊息佇列”頭部的一條記錄。
6) subscribe端啟動時,如果發現自己的自己的“訂閱者訊息佇列”有殘存記錄,那麼將會首先消費這些記錄,然後再去訂閱。
--------------------------------------------------------------非持久化訂閱-------------------------------------------------------
PrintListener.java:訂閱者訊息處理器 Java代碼 public class PrintListener extends JedisPubSub{ @Override public void onMessage(String channel, String message) { String time = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"); System.out.println("message receive:" + message + ",channel:" + channel + "..." + time); //此處我們可以取消訂閱 if(message.equalsIgnoreCase("quit")){ this.unsubscribe(channel); } } ... }
public class PrintListener extends JedisPubSub{@Overridepublic void onMessage(String channel, String message) {String time = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");System.out.println("message receive:" + message + ",channel:" + channel + "..." + time);//此處我們可以取消訂閱if(message.equalsIgnoreCase("quit")){this.unsubscribe(channel);}}...}
PubClient.java:訊息發布端 Java代碼 public class PubClient { private Jedis jedis;// public PubClient(String host,int port){ jedis = new Jedis(host,port); } public void pub(String channel,String message){ jedis.publish(channel, message); } public void close(String channel){ jedis.publish(channel, "quit"); jedis.del(channel);// } }
public class PubClient {private Jedis jedis;//public PubClient(String host,int port){jedis = new Jedis(host,port);}public void pub(String channel,String message){jedis.publish(channel, message);}public void close(String channel){jedis.publish(channel, "quit");jedis.del(channel);//}}
SubClient.java:訊息訂閱端 Java代碼 public class SubClient { private Jedis jedis;// public SubClient(String host,int port){ jedis = new Jedis(host,port); } public void sub(JedisPubSub listener,String channel){ jedis.subscribe(listener, channel); //此處將會阻塞,在client代碼層級為JedisPubSub在處理訊息時,將會“獨佔”連結 //並且採取了while迴圈的方式,偵聽訂閱的訊息 // } }
public class SubClient {private Jedis jedis;//public SubClient(String host,int port){jedis = new Jedis(host,port);}public void sub(JedisPubSub listener,String channel){jedis.subscribe(listener, channel);//此處將會阻塞,在client代碼層級為JedisPubSub在處理訊息時,將會“獨佔”連結//並且採取了while迴圈的方式,偵聽訂閱的訊息//}}
PubSubTestMain.java:測試引導類 Java代碼 public class PubSubTestMain { /** * @param args */ public static void main(String[] args) throws Exception{ PubClient pubClient = new PubClient(Constants.host, Constants.port); final String channel = "pubsub-channel"; pubClient.pub(channel, "before1"); pubClient.pub(channel, "before2"); Thread.sleep(2000); //訊息訂閱著非常特殊,需要獨佔連結,因此我們需要為它建立新的連結; //此外,jedis用戶端的實現也保證了“連結獨佔”的特性,sub方法將一直阻塞, //直到調用listener.unsubscribe方法 Thread subThread = new Thread(new Runnable() { @Override public void run() { try{ SubClient subClient = new SubClient(Constants.host, Constants.port); System.out.println("----------subscribe operation begin-------"); JedisPubSub listener = new PrintListener(); //在API層級,此處為輪詢操作,直到unsubscribe調用,才會返回 subClient.sub(listener, channel); System.out.println("----------subscribe operation end-------"); }catch(Exception e){ e.printStackTrace(); } } }); subThread.start(); int i=0; while(i < 10){ String message = RandomStringUtils.random(6, true, true);//apache-commons pubClient.pub(channel, message); i++; Thread.sleep(1000); } //被動關閉指示,如果通道中,訊息發行者確定通道需要關閉,那麼就發送一個“quit” //那麼在listener.onMessage()中接收到“quit”時,其他訂閱client將執行“unsubscribe”操作。 pubClient.close(channel); //此外,你還可以這樣取消訂閱 //listener.unsubscribe(channel); } }
public class PubSubTestMain {/** * @param args */public static void main(String[] args) throws Exception{PubClient pubClient = new PubClient(Constants.host, Constants.port);final String channel = "pubsub-channel";pubClient.pub(channel, "before1");pubClient.pub(channel, "before2");Thread.sleep(2000);//訊息訂閱著非常特殊,需要獨佔連結,因此我們需要為它建立新的連結;//此外,jedis用戶端的實現也保證了“連結獨佔”的特性,sub方法將一直阻塞,//直到調用listener.unsubscribe方法Thread subThread = new Thread(new Runnable() {@Overridepublic void run() {try{SubClient subClient = new SubClient(Constants.host, Constants.port);System.out.println("----------subscribe operation begin-------");JedisPubSub listener = new PrintListener();//在API層級,此處為輪詢操作,直到unsubscribe調用,才會返回subClient.sub(listener, channel);System.out.println("----------subscribe operation end-------");}catch(Exception e){e.printStackTrace();}}});subThread.start();int i=0;while(i < 10){String message = RandomStringUtils.random(6, true, true);//apache-commonspubClient.pub(channel, message);i++;Thread.sleep(1000);}//被動關閉指示,如果通道中,訊息發行者確定通道需要關閉,那麼就發送一個“quit”//那麼在listener.onMessage()中接收到“quit”時,其他訂閱client將執行“unsubscribe”操作。pubClient.close(channel);//此外,你還可以這樣取消訂閱//listener.unsubscribe(channel);}}
--------------------------------------------------------------持久化訂閱-------------------------------------------------------
PPrintListener.java Java代碼 public class PPrintListener extends JedisPubSub{ private String clientId; private PSubHandler handler; public PPrintListener(String clientId,Jedis jedis){ this.clientId = clientId; handler = new PSubHandler(jedis); } @Override public void onMessage(String channel, String message) { //此處我們可以取消訂閱 if(message.equalsIgnoreCase("quit")){ this.unsubscribe(channel); } handler.handle(channel, message); } private void message(String channel,String message){ String time = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"); System.out.println("message receive:" + message + ",channel:" + channel + "..." + time); } @Override public void onPMessage(String pattern, String channel, String message) { System.out.println("message receive:" + message + ",pattern channel:" + channel); } @Override public<