• Redis编程实践【pub/sub】


    原文:http://shift-alt-ctrl.iteye.com/blog/1867454

        Redis或许已经在很多企业开始推广并试水,本文也根据个人的实践,简单描述一下Redis在实际开发过程中的使用(部署与架构,稍后介绍),程序执行环境为java + jedis,关于spring下如何集成redis-api,稍后介绍吧。

    前言:下载redis-2.6.2,安装好redis之后,请在redis.conf文件中,将如下3个配置属性开启(仅供测试使用):

    Xml代码  收藏代码
    1. ##客户端链接的端口,也是server端侦听client链接的端口    
    2. ##每个client实例,都将和server在此端口上建立tcp长链接    
    3. port 6379    
    4. ## server端绑定的ip地址,如果一个物理机器有多个网络接口时,可以明确指定为某个网口的ip地址    
    5. bind 127.0.0.1    
    6. ##链接中io操作空闲时间,如果在指定时间内,没有IO操作,链接将会被关闭    
    7. ##此属性和TCP链接中的timeout选项一样,建议设置为0,很多时候,我们一个应用也只会有一个redis实例    
    8. ##不过,如果你使用连接池的话,你需要对此参数做额外的考虑。    
    9. 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代码  收藏代码
    1. public class PrintListener extends JedisPubSub{  
    2.   
    3.     @Override  
    4.     public void onMessage(String channel, String message) {  
    5.         String time = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");  
    6.         System.out.println("message receive:" + message + ",channel:" + channel + "..." + time);  
    7.         //此处我们可以取消订阅  
    8.         if(message.equalsIgnoreCase("quit")){  
    9.             this.unsubscribe(channel);  
    10.         }  
    11.     }  
    12. ...  
    13. }  

     

    PubClient.java:消息发布端

    Java代码  收藏代码
    1. public class PubClient {  
    2.   
    3.     private Jedis jedis;//  
    4.     public PubClient(String host,int port){  
    5.         jedis = new Jedis(host,port);  
    6.     }  
    7.       
    8.     public void pub(String channel,String message){  
    9.         jedis.publish(channel, message);  
    10.     }  
    11.       
    12.     public void close(String channel){  
    13.         jedis.publish(channel, "quit");  
    14.         jedis.del(channel);//  
    15.     }  
    16.   
    17. }  

    SubClient.java:消息订阅端

    Java代码  收藏代码
    1. public class SubClient {  
    2.   
    3.     private Jedis jedis;//  
    4.       
    5.     public SubClient(String host,int port){  
    6.         jedis = new Jedis(host,port);  
    7.     }  
    8.       
    9.     public void sub(JedisPubSub listener,String channel){  
    10.         jedis.subscribe(listener, channel);  
    11.         //此处将会阻塞,在client代码级别为JedisPubSub在处理消息时,将会“独占”链接  
    12.         //并且采取了while循环的方式,侦听订阅的消息  
    13.         //  
    14.     }  
    15.   
    16. }  

    PubSubTestMain.java:测试引导类

    Java代码  收藏代码
    1. public class PubSubTestMain {  
    2.   
    3.     /** 
    4.      * @param args 
    5.      */  
    6.     public static void main(String[] args) throws Exception{  
    7.         PubClient pubClient = new PubClient(Constants.host, Constants.port);  
    8.         final String channel = "pubsub-channel";  
    9.         pubClient.pub(channel, "before1");  
    10.         pubClient.pub(channel, "before2");  
    11.         Thread.sleep(2000);  
    12.         //消息订阅着非常特殊,需要独占链接,因此我们需要为它创建新的链接;  
    13.         //此外,jedis客户端的实现也保证了“链接独占”的特性,sub方法将一直阻塞,  
    14.         //直到调用listener.unsubscribe方法  
    15.         Thread subThread = new Thread(new Runnable() {  
    16.             @Override  
    17.             public void run() {  
    18.                 try{  
    19.                     SubClient subClient = new SubClient(Constants.host, Constants.port);  
    20.                     System.out.println("----------subscribe operation begin-------");  
    21.                     JedisPubSub listener = new PrintListener();  
    22.                     //在API级别,此处为轮询操作,直到unsubscribe调用,才会返回  
    23.                     subClient.sub(listener, channel);  
    24.                     System.out.println("----------subscribe operation end-------");  
    25.                 }catch(Exception e){  
    26.                     e.printStackTrace();  
    27.                 }  
    28.                   
    29.             }  
    30.         });  
    31.         subThread.start();  
    32.         int i=0;  
    33.         while(i < 10){  
    34.             String message = RandomStringUtils.random(6, true, true);//apache-commons  
    35.             pubClient.pub(channel, message);  
    36.             i++;  
    37.             Thread.sleep(1000);  
    38.         }  
    39.         //被动关闭指示,如果通道中,消息发布者确定通道需要关闭,那么就发送一个“quit”  
    40.         //那么在listener.onMessage()中接收到“quit”时,其他订阅client将执行“unsubscribe”操作。  
    41.         pubClient.close(channel);  
    42.         //此外,你还可以这样取消订阅  
    43.         //listener.unsubscribe(channel);  
    44.   
    45.     }  
    46.   
    47. }  

    --------------------------------------------------------------持久化订阅-------------------------------------------------------

     基本思路:当订阅者订阅消息时,将此订阅者信息添加到一个列表中,此列表为“所有订阅者列表”,同时为每个订阅者都创建一个保存消息(内容或者消息ID)的队列,消息发布者将每条消息都添加到每个订阅者的队列中。

    如下实现仅供参考,有很多更优的实现方式。

    PPrintListener.java

    Java代码  收藏代码
    1. public class PPrintListener extends JedisPubSub{  
    2.   
    3.     private String clientId;  
    4.     private PSubHandler handler;  
    5.       
    6.     public PPrintListener(String clientId,Jedis jedis){  
    7.         this.clientId = clientId;  
    8.         handler = new PSubHandler(jedis);  
    9.     }  
    10.       
    11.     @Override  
    12.     public void onMessage(String channel, String message) {  
    13.         //此处我们可以取消订阅  
    14.         if(message.equalsIgnoreCase("quit")){  
    15.             this.unsubscribe(channel);  
    16.         }  
    17.         handler.handle(channel, message);//触发当前订阅者从自己的消息队列中移除消息  
    18.     }  
    19.       
    20.     private void message(String channel,String message){  
    21.         String time = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");  
    22.         System.out.println("message receive:" + message + ",channel:" + channel + "..." + time);  
    23.     }  
    24.   
    25.     @Override  
    26.     public void onPMessage(String pattern, String channel, String message) {  
    27.         System.out.println("message receive:" + message + ",pattern channel:" + channel);  
    28.           
    29.     }  
    30.   
    31.     @Override  
    32.     public void onSubscribe(String channel, int subscribedChannels) {  
    33.         handler.subscribe(channel);  
    34.         System.out.println("subscribe:" + channel + ";total channels : " + subscribedChannels);  
    35.           
    36.     }  
    37.   
    38.     @Override  
    39.     public void onUnsubscribe(String channel, int subscribedChannels) {  
    40.         handler.unsubscribe(channel);  
    41.         System.out.println("unsubscribe:" + channel + ";total channels : " + subscribedChannels);  
    42.           
    43.     }  
    44.   
    45.     @Override  
    46.     public void onPUnsubscribe(String pattern, int subscribedChannels) {  
    47.         System.out.println("unsubscribe pattern:" + pattern + ";total channels : " + subscribedChannels);  
    48.           
    49.     }  
    50.   
    51.     @Override  
    52.     public void onPSubscribe(String pattern, int subscribedChannels) {  
    53.         System.out.println("subscribe pattern:" + pattern + ";total channels : " + subscribedChannels);       
    54.     }  
    55.       
    56.     @Override  
    57.     public void unsubscribe(String... channels) {  
    58.         super.unsubscribe(channels);  
    59.         for(String channel : channels){  
    60.             handler.unsubscribe(channel);  
    61.         }  
    62.     }  
    63.       
    64.     class PSubHandler {  
    65.   
    66.         private Jedis jedis;  
    67.         PSubHandler(Jedis jedis){  
    68.             this.jedis = jedis;  
    69.         }  
    70.         public void handle(String channel,String message){  
    71.             int index = message.indexOf("/");  
    72.             if(index < 0){  
    73.                 return;  
    74.             }  
    75.             Long txid = Long.valueOf(message.substring(0,index));  
    76.             String key = clientId + "/" + channel;  
    77.             while(true){  
    78.                     String lm = jedis.lindex(key, 0);//获取第一个消息  
    79.                     if(lm == null){  
    80.                         break;  
    81.                     }  
    82.                     int li = lm.indexOf("/");  
    83.                     //如果消息不合法,删除并处理  
    84.                     if(li < 0){  
    85.                         String result = jedis.lpop(key);//删除当前message  
    86.                         //为空  
    87.                         if(result == null){  
    88.                             break;  
    89.                         }  
    90.                         message(channel, lm);  
    91.                         continue;  
    92.                     }  
    93.                     Long lxid = Long.valueOf(lm.substring(0,li));//获取消息的txid  
    94.                     //直接消费txid之前的残留消息  
    95.                     if(txid >= lxid){  
    96.                         jedis.lpop(key);//删除当前message  
    97.                         message(channel, lm);  
    98.                         continue;  
    99.                     }else{  
    100.                         break;  
    101.                     }  
    102.             }  
    103.         }  
    104.           
    105.         public void subscribe(String channel){  
    106.             String key = clientId + "/" + channel;  
    107.             boolean exist = jedis.sismember(Constants.SUBSCRIBE_CENTER,key);  
    108.             if(!exist){  
    109.                 jedis.sadd(Constants.SUBSCRIBE_CENTER, key);  
    110.             }  
    111.         }  
    112.           
    113.         public void unsubscribe(String channel){  
    114.             String key = clientId + "/" + channel;  
    115.             jedis.srem(Constants.SUBSCRIBE_CENTER, key);//从“活跃订阅者”集合中删除  
    116.             jedis.del(key);//删除“订阅者消息队列”  
    117.         }  
    118.     }  
    119. }  

    PPubClient.java

    Java代码  收藏代码
    1. public class PPubClient {  
    2.   
    3.     private Jedis jedis;//  
    4.     public PPubClient(String host,int port){  
    5.         jedis = new Jedis(host,port);  
    6.     }  
    7.       
    8.     /** 
    9.      * 发布的每条消息,都需要在“订阅者消息队列”中持久 
    10.      * @param message 
    11.      */  
    12.     private void put(String message){  
    13.         //期望这个集合不要太大  
    14.         Set<String> subClients = jedis.smembers(Constants.SUBSCRIBE_CENTER);  
    15.         for(String clientKey : subClients){  
    16.             jedis.rpush(clientKey, message);  
    17.         }  
    18.     }  
    19.       
    20.     public void pub(String channel,String message){  
    21.         //每个消息,都有具有一个全局唯一的id  
    22.         //txid为了防止订阅端在数据处理时“乱序”,这就要求订阅者需要解析message  
    23.         Long txid = jedis.incr(Constants.MESSAGE_TXID);  
    24.         String content = txid + "/" + message;  
    25.         //非事务  
    26.         this.put(content);  
    27.         jedis.publish(channel, content);//为每个消息设定id,最终消息格式1000/messageContent  
    28.           
    29.     }  
    30.       
    31.     public void close(String channel){  
    32.         jedis.publish(channel, "quit");  
    33.         jedis.del(channel);//删除  
    34.     }  
    35.       
    36.     public void test(){  
    37.         jedis.set("pub-block", "15");  
    38.         String tmp = jedis.get("pub-block");  
    39.         System.out.println("TEST:" + tmp);  
    40.     }  
    41.   
    42.   
    43. }  

    PPSubClient.java

    Java代码  收藏代码
    1. public class PSubClient {  
    2.   
    3.     private Jedis jedis;//  
    4.     private JedisPubSub listener;//单listener  
    5.       
    6.     public PSubClient(String host,int port,String clientId){  
    7.         jedis = new Jedis(host,port);  
    8.         listener = new PPrintListener(clientId, new Jedis(host, port));  
    9.     }  
    10.       
    11.     public void sub(String channel){  
    12.         jedis.subscribe(listener, channel);  
    13.     }  
    14.       
    15.     public void unsubscribe(String channel){  
    16.         listener.unsubscribe(channel);  
    17.     }  
    18.       
    19. }  

    PPubSubTestMain.java

    Java代码  收藏代码
      1. public class PPubSubTestMain {  
      2.   
      3.     /** 
      4.      * @param args 
      5.      */  
      6.     public static void main(String[] args) throws Exception{  
      7.         PPubClient pubClient = new PPubClient(Constants.host, Constants.port);  
      8.         final String channel = "pubsub-channel-p";  
      9.         final PSubClient subClient = new PSubClient(Constants.host, Constants.port,"subClient-1");  
      10.         Thread subThread = new Thread(new Runnable() {  
      11.               
      12.             @Override  
      13.             public void run() {  
      14.                 System.out.println("----------subscribe operation begin-------");  
      15.                 //在API级别,此处为轮询操作,直到unsubscribe调用,才会返回  
      16.                 subClient.sub(channel);  
      17.                 System.out.println("----------subscribe operation end-------");  
      18.                   
      19.             }  
      20.         });  
      21.         subThread.setDaemon(true);  
      22.         subThread.start();  
      23.         int i = 0;  
      24.         while(i < 2){  
      25.             String message = RandomStringUtils.random(6, true, true);//apache-commons  
      26.             pubClient.pub(channel, message);  
      27.             i++;  
      28.             Thread.sleep(1000);  
      29.         }  
      30.         subClient.unsubscribe(channel);  
      31.     }  
      32.       
      33. }  
  • 相关阅读:
    discuz开发笔记
    响应式布局
    timedelta
    图片轮播
    性能
    事件捕获
    git
    css hacks
    AFNetworking 网络错误提示data转换字符串
    常见HTTP错误代码
  • 原文地址:https://www.cnblogs.com/shihaiming/p/6054192.html
Copyright © 2020-2023  润新知