目录
前言
生产者和消费者
发布和订阅
Java实现
注意
前言
随着业务复杂, 业务的项目依赖关系增强, 使用消息队列帮助系统降低耦合度.发布订阅(pub/sub)是一种消息通信模式,主要目的是解除消息发布者、消息订阅者之间的耦合
订阅分布本身也是一种生产者消费者模式, 订阅者是消费者, 发布者是生产者.
订阅发布模式, 发布者发布消息后, 只要有订阅方, 则多个订阅方会收到同样的消息
生产者消费者模式, 生产者往队列里放入消息, 由多个消费者对一条消息进行抢占.
订阅分布模式可以将一些不着急完成的工作放到其他进程或者线程中进行离线处理.
pub/sub的特点
(1)时间非耦合
发布者和订阅者不必同时在线,它们不必同时参与交互
(2)空间非耦合
发布者和订阅者不必相互知道对方所在的位置
(3)同步非耦合
发布者/订阅者是异步模式,发布者可不断地生产消息,订阅者则可异步地得到消息通知
pub/sub的使用场景
基于pub/sub的特点,他的典型使用场景就是实时消息系统,比如即时聊天,群聊等功能,还常用作减轻高并发的I/O写压力,例如大量的写日志操作,如果实时写入日志文件或者数据库,会造成I/O超负荷,降低系统性能,那么就可以用pub/sub方式,写日志时先不进行写操作,而是向日志频道发布一条日志消息,然后有一个单独的日志程序来订阅日志频道,异步的读取日志消息写入文件或数据库
Redis中的订阅发布模式, 当没有订阅者时, 消息会被直接丢弃(Redis不会持久化保存消息)
生产者和消费者
生产者使用Redis中的list数据结构进行实现, 将待处理的消息塞入到消息队列中.
具体内容放在下次“消息通道异步处理”讲
发布和订阅
在Redis Pubsub中, 一个频道(channel)相当于一个消息队列,
“发布/订阅”模式包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。
发布者发送消息的命令是PUBLISH,用法是PUBLISH channel message,如向channel.1说一声“hi”:
redis>PUBLISH channel.1 hi
(integer) 0
这样消息就发出去了。返回值表示接收到这条消息的订阅者数量。发出去的消息不会被持久化,也就是说当客户端订阅channel.1后只能收到后续发布到该频道的消息,之前发送到就收不到了。
订阅频道的命令是SUBSCRIBE,可以同时订阅多个频道,用法是 SUBSCRIBE channel [channel ...]。
redis>SUBSCRIBE channel.1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel.1"
3) (integer) 1
执行SUBSCRIBE命令后进入订阅状态,处于此状态下客户端不能使用除SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBE/PUNSUBSCRIBE这四个属于“发布/订阅”模式之外的命令,否则会报错。
进入订阅模式后客户端可能收到三种类型的回复。每种类型的回复都包含3个值,第一个值是消息的类型,根据消息类型的不同,第二第三个值的含义也不同。消息类型可能的取值有:
1)Subscribe。表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个值是当前客户端订阅的频道数。
2)message。这个类型的回复表示收到的消息。第二个值表示产生消息的频道名称,第三个值是消息内容。
3)unsubscribe。表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数量,当此值为0时客户端会退出订阅状态。
redis 将所有频道的订阅关系都保存在 pubsub_channels 字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端。当某频道有新消息时,就会查找对应的链表,向链表中每个客户端发送通知
当一个连接通过subscribe或者psubscribe订阅通道后就进入订阅模式。在这种模式除了再订阅额外的通道或者用unsubscribe或者punsubscribe命令退出订阅模式,就不能再发送其他命令。另外使用 psubscribe命令订阅多个通配符通道,如果一个消息匹配上了多个通道模式的话,会多次收到同一个消息。
java 类实现
a.订阅
1 package com.dengzy.redis; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import redis.clients.jedis.Jedis; 7 import redis.clients.jedis.JedisPubSub; 8 9 public class Subscriber extends JedisPubSub { 10 11 private static Logger logger = LoggerFactory.getLogger(Subscriber.class); 12 13 @Override 14 public void onMessage(String channel, String message) { 15 logger.warn("Message received. Channel: [" + channel + "], Msg: [" + message + "] )"); 16 } 17 18 @Override 19 public void onPMessage(String pattern, String channel, String message) { 20 System.out.println("onPMessage: channel[" + channel + "], message[" + message + "]"); 21 } 22 23 @Override 24 public void onSubscribe(String channel, int subscribedChannels) { 25 System.out.println("onSubscribe: channel[" + channel + "]," + "subscribedChannels[" + subscribedChannels + "]"); 26 } 27 28 @Override 29 public void onUnsubscribe(String channel, int subscribedChannels) { 30 System.out.println("onUnsubscribe: channel[" + channel + "], " + "subscribedChannels[" + subscribedChannels + "]"); 31 } 32 33 @Override 34 public void onPUnsubscribe(String pattern, int subscribedChannels) { 35 System.out.println("onPUnsubscribe: pattern[" + pattern + "]," + "subscribedChannels[" + subscribedChannels + "]"); 36 } 37 38 @Override 39 public void onPSubscribe(String pattern, int subscribedChannels) { 40 System.out.println("onPSubscribe: pattern[" + pattern + "], " + "subscribedChannels[" + subscribedChannels + "]"); 41 } 42 43 public static void main(String[] args) { 44 Jedis jr = null; 45 try { 46 jr = new Jedis("192.168.1.10", 6379, 0);// redis服务地址和端口号 47 Subscriber sp = new Subscriber(); 48 sp.proceed(jr.getClient(), "news.share", "news.blog"); 49 // sp.proceedWithPatterns(jr.getClient(), "news.*"); 50 } catch (Exception e) { 51 e.printStackTrace(); 52 } finally { 53 if (jr != null) { 54 jr.disconnect(); 55 } 56 } 57 } 58 }
b.发布
3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 import redis.clients.jedis.Jedis; 6 7 import java.io.BufferedReader; 8 import java.io.IOException; 9 import java.io.InputStreamReader; 10 11 public class Publisher { 12 13 private static final Logger logger = LoggerFactory.getLogger(Publisher.class); 14 15 private final Jedis publisherJedis; 16 17 private final String channel; 18 19 public Publisher(Jedis publisherJedis, String channel) { 20 this.publisherJedis = publisherJedis; 21 this.channel = channel; 22 } 23 24 public void start() { 25 logger.info("Type your message (quit for terminate)"); 26 try { 27 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 28 29 while (true) { 30 String line = reader.readLine(); 31 32 if (!"quit".equals(line)) { 33 publisherJedis.publish(channel, line); 34 } else { 35 break; 36 } 37 } 38 39 } catch (IOException e) { 40 logger.error("IO failure while reading input, e"); 41 } 42 } 43 }
3. main
1 import org.slf4j.Logger; 2 import org.slf4j.LoggerFactory; 3 import redis.clients.jedis.Jedis; 4 import redis.clients.jedis.JedisPool; 5 import redis.clients.jedis.JedisPoolConfig; 6 7 public class Program { 8 private static Logger logger = LoggerFactory.getLogger(Program.class); 9 10 public static final String CHANNEL_NAME = "commonChannel"; 11 12 public static void main(String[] args) throws Exception { 13 14 JedisPoolConfig poolConfig = new JedisPoolConfig(); 15 16 JedisPool jedisPool = new JedisPool(poolConfig, "192.168.1.10", 6379, 0); 17 18 final Jedis subscriberJedis = jedisPool.getResource(); 19 20 final Subscriber subscriber = new Subscriber(); 21 22 new Thread(new Runnable() { 23 @Override 24 public void run() { 25 try { 26 logger.info("Subscribing to "commonChannel". This thread will be blocked."); 27 subscriberJedis.subscribe(subscriber, CHANNEL_NAME); 28 logger.info("Subscription ended."); 29 } catch (Exception e) { 30 logger.error("Subscribing failed." + e.getMessage()); 31 } 32 } 33 }).start(); 34 35 Jedis publisherJedis = jedisPool.getResource(); 36 37 new Publisher(publisherJedis, CHANNEL_NAME).start(); 38 39 subscriber.unsubscribe(); 40 jedisPool.returnResource(subscriberJedis); 41 jedisPool.returnResource(publisherJedis); 42 } 43 }
注意
1.在Jedis中订阅方处理是采用异步的方式,
如同步的方式,在do-while循环中, 会等到当前消息处理完毕才能够处理下一条消息, 这样会导致当入队列消息量过大的时候, redis链接被强制关闭.
2.“单点问题”,如果发布者的消息过多,一台redis-server处理不过来,redis还没支持负载均衡集群,主从配置在“消息过多”情况下还是无能为力,接收消息的压力始终都得在主的压力上面。这种情况下只能人为的将发布者的消息按照业务拆分,将某些消息发布到另外一台redis server上面去。
3.通过负载均衡集群来增加自己的接收消息的能力,通过主从配置解决 redis-server的消息分发能力不够。
4.如果redis-server的订阅端处理能力不足怎么办?这一点对于redis sever非常危险。因为redis-server会将消息存储在redis server 服务端内存中,如果订阅端的处理始终处理缓慢,那么redis server的内存就会不断变大。