Redis的Pub/Sub机制存在的问题以及解决方案
Redis的Pub/Sub机制使用非常简单的方式实现了观察者模式,但是在使用过程中我们发现,它仅仅是实现了发布订阅机制,但是很多的场景没有考虑到。例如一下的几种场景:
1.数据可靠性无法保证
一个redis_cli发送消息的时候,消息是无状态的,也就是说负责发送消息的redis_cli只管发送消息,并不会理会消息是否被订阅者接收到,也不会理会是否在传输过程中丢失,即对于发布者来说,消息是”即发即失”的。
2.扩展性差
不能通过增加消费者来加快消耗发布者的写入的数据,如果发布者发布的消息很多,则数据阻塞在通道中已等待被消费着来消耗。阻塞时间越久,数据丢失的风险越大(网络或者服务器的一个不稳定就会导致数据的丢失)
3.资源消耗高
在pub/sub中消息发布者不需要独占一个Redis的链接,而消费者则需要单独占用一个Redis的链接,在java中便不得独立出分出一个线程来处理消费者。这种场景一般对应这多个消费者,此时则有着过高的资源消耗。
对于如上的几种不足,如果在项目中需要考虑的话可以使用JMS来实现该功能。JMS提供了消息的持久化/耐久性等各种企业级的特性。如果依然想使用Redis来实现并做一些数据的持久化操作,则可以根据JMS的特性来通过Redis模拟出来.
模拟的步骤如下:
1.subscribe端首先向一个Set集合中增加“订阅者ID”,此Set集合保存了“活跃订阅”者,订阅者ID标记每个唯一的订阅者,例如:sub:email,sub:web。此SET称为“活跃订阅者集合”
2.subcribe端开启订阅操作,并基于Redis创建一个以“订阅者ID”为KEY的LIST数据结构,此LIST中存储了所有的尚未消费的消息。此LIST称为“订阅者消息队列”
3.publish端:每发布一条消息之后,publish端都需要遍历“活跃订阅者集合”,并依次向每个“订阅者消息队列”尾部追加此次发布的消息。到此为止,我们可以基本保证,发布的每一条消息,都会持久保存在每个“订阅者消息队列”中。
4.subscribe端,每收到一个订阅消息,在消费之后,必须删除自己的“订阅者消息队列”头部的一条记录。subscribe端启动时,如果发现自己的自己的“订阅者消息队列”有残存记录,那么将会首先消费这些记录,然后再去订阅。
实现如下:
public class PubSubListener extends JedisPubSub{ private String clientId; private RedisHandler redisHandler; public PubSubListener(String clientId, Jedis jedis) { this.clientId = clientId; this.redisHandler = new RedisHandler(jedis); } @Override public void onMessage(String channel, String message) { if ("exit".equals(message)) { redisHandler.onUnsubscribe(channel); } redisHandler.hanlder(channel,message); } @Override public void onSubscribe(String channel, int subscribedChannels) { redisHandler.subscribe(channel); } @Override public void onUnsubscribe(String channel, int subscribedChannels) { redisHandler.onUnsubscribe(channel); } class RedisHandler{ private Jedis jedis =null; public RedisHandler(Jedis jedis) { this.jedis = jedis; } /** * 订阅操作步骤: * 1.判断clientID是否在PERSITS_SUB队列中 * 2.如果在队列中说明已经订阅,或则把clientID添加到队列中 * @param channel */ public void subscribe(String channel){ String key = clientId + "/" + channel; boolean isExists = this.jedis.sismember("PERSITS_SUB",key); if(!isExists){ this.jedis.sadd("PERSITS_SUB",key); } } /** * 取消订阅 * @param channel */ public void onUnsubscribe(String channel){ String key = clientId + "/" + channel; //从订阅者队列中删除 this.jedis.srem("",key); //删除订阅者消息队列 this.jedis.del(channel); } public void hanlder(String channel,String message){ int index = message.indexOf("/"); if(index < 0){ //消息不合法,丢弃 return; } Long txid = Long.valueOf(message.substring(0,index)); String key = clientId + "/" + channel; while(true){ String lm = this.jedis.lindex(key,0);//获取第一个消息 if(lm == null){ break; } int li = lm.indexOf("/"); if(li < 0){ //消息不合法 String result = this.jedis.lpop(key); if(result == null){ break; } message(channel,message); continue; } long lmid = Long.parseLong(lm.substring(0,li)); if(txid >= lmid){ this.jedis.lpop(key); message(channel,message); continue; }else{ break; } } } } private void message(String channel, String message) { System.out.println("receive message " + message); } }
public class SubClient { private Jedis jedis = null; private PubSubListener listener = null; public SubClient(Jedis jedis, PubSubListener listener) { this.jedis = jedis; this.listener = listener; } public void subscribe(String channel){ jedis.subscribe(listener,channel); } public void onUnsubscribe(String channel){ listener.unsubscribe(channel); } }
public class PubClient { private Jedis jedis = null; public PubClient(String host) { this.jedis = new Jedis(host); } public void put(String message){ Set<String> clients = this.jedis.smembers("PERSITS_SUB"); for(String client : clients){ //在每个客户端对应的消息队列中持久化消息 this.jedis.rpush(client,message); } } /** * 每个消息,都有具有一个全局唯一的id * txid为了防止订阅端在数据处理时“乱序”,这就要求订阅者需要解析message * @param channel * @param message */ public void publish(String channel, String message) { Long txid = jedis.incr("MESSAGE_TXID"); String content = txid + "/" + message; this.put(content); jedis.publish(channel, content);//为每个消息设定id,最终消息格式1000/messageContent } public void close(String channel){ jedis.publish(channel, "exit"); jedis.del(channel);//删除 } }