【编码】封装RedisPubSub工具
基本介绍
核心原理:利用Redis的List列表实现,发布事件对应rpush,订阅事件对应lpop
问题一:Redis不是自带Pub/Sub吗?
redis自带的pub/sub有两个问题:
1.如果发布消息期间订阅方没有连到redis,那么这条消息就收不到了,即使重新连接上来也收不到
2.redis内部是用一个线程给所有订阅连接推数据的,V生产> V消费 的情况下还会主动断开连接,有性能隐患。感兴趣的可以多了解一下它的原理。
问题二:要实现怎样一个工具,或者说想要什么样的效果?
效果就是得到一个service对象,这个对象有以下两个重要功能:
1.有个publish方法可以调用,用来灵活地发布消息。想发布什么就发布什么,想给哪个topic发送就给哪个topic发送。
2.可以预定义一些订阅者,定义好当收到某个topic的消息后,该做什么处理。
编码内容
(一)接口定义
第一步要做的就是定义接口,一个是发布接口,我们需要这样一个接口来发布消息,消息内容可以是任何形式的对象
public interface MessagePublisher { /** * 发布消息 * @param topic 主题 * @param msg 消息内容 */ void publish(String topic, Object msg); }
第二个是订阅接口,我们需要依此实现观察者模式
public interface MessageConsumer { /** * 获取此消费者订阅的topic * @return 订阅topic */ String getTopic(); /** * 回调方法,收到消息后,此方法被触发 * @param topic topic * @param msg 消息内容 */ void onMessage(String topic, Object msg); }
第三个就是转换接口,已知Redis不能直接存储Java对象,所以必须进行转换,这里我们选择用String形式进行存储。所以我们需要一个类型转换工具
public interface Translator { /** * 将对象序列化为字符串 * @param obj 对象 * @return 字符串 */ String serialize(Object obj); /** * 将字符串反序列化为对象 * @param str 字符串 * @return 对象 */ Object deserialize(String str); }
(二)转换器实现——JsonTranslator
问题一:取出数据后如何转换成正确的对象?
在写入redis的时候同时也写入该对象的类型信息,然后取出的时候利用该类型信息进行转换即可。
public class JsonTranslator implements Translator { private static ObjectMapper MAPPER = new ObjectMapper(); /** * 缓存类信息,优化速度 */ private Map<String, Class> classCache = new HashMap<>(); @Override public String serialize(Object obj) { Message message = new Message(); message.setClazz(obj.getClass().getName()); message.setData(encode(obj)); return encode(message); } @Override public Object deserialize(String str) { Message message = decode(str, Message.class); String className = message.getClazz(); Class clazz = classCache.get(className); if(clazz != null) return decode(message.getData(), clazz); try { clazz = Class.forName(className); classCache.put(className, clazz); return decode(message.getData(), clazz); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private String encode(Object obj) { try { return MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private <T> T decode(String str, Class<T> clazz) { try { return (T) MAPPER.readValue(str, Message.class); } catch (IOException e) { throw new RuntimeException(e); } } @Data class Message { //保存类信息,是为了反序列化能够得到正确类型的对象 /** * 类名(含路径) */ private String clazz; /** * 序列化后的对象 */ private String data; } }
(三)核心实现——RedisPubSub
问题一:Redis配置如何处理?
我们将Redis的配置与这个MQ解耦,让用户配置连接池后再注入进来即可。
问题二:如何知道要监听哪些topic?
我们把容器中的Consumer实现类都注入进来,就可以通过getTopic方法得到总共需要监听哪些topic。
问题三:如何进行监听?
每个需要监听的topic开一个线程进行监听,监听方法就是循环调用blpop。
问题四:监听到消息后如何进行通知?
当得到topic的消息的时候,就回调订阅此topic的consumer的onMessage方法。
问题五:如何启动和关闭监听?
我们给MQ类提供两个方法start和stop。在注入容器的时候指明这两个分别是init和destroy方法,这样它就能随着容器启动和停止了。
public class RedisPubSub implements MessagePublisher{ //外部注入信息 private JedisPool jedisPool; private List<MessageConsumer> consumerList; /** * 对象和字符串的转换器,默认使用JsonTranslator */ private Translator translator = new JsonTranslator(); //内部信息 /** * key:topic * value:此topic的订阅者 */ private Map<String, List<MessageConsumer>> subcribeInfo; private List<MessageListener> listeners; public void setJedisPool(JedisPool jedisPool) { this.jedisPool = jedisPool; } public void setConsumerList(List<MessageConsumer> consumerList) { this.consumerList = consumerList; subcribeInfo = new HashMap<>(); String topic; List<MessageConsumer> topicConsumers; //注入消费者后,整理好订阅情况 for(MessageConsumer consumer : consumerList) { topic = consumer.getTopic(); topicConsumers = subcribeInfo.get(topic); if(topicConsumers == null) { topicConsumers = new ArrayList<>(); subcribeInfo.put(topic, topicConsumers); } topicConsumers.add(consumer); } } public void setTranslator(Translator translator) { this.translator = translator; } public void publish(String topic, Object msg) { Jedis jedis = jedisPool.getResource(); jedis.rpush(topic,translator.serialize(msg)); jedis.close(); } public void start() { MessageListener listener; //每个topic开一个监听线程进行监听 for(String topic : subcribeInfo.keySet()) { listener = new MessageListener(topic, subcribeInfo.get(topic)); listener.start(); listeners.add(listener); } } public void stop() { //关闭所有监听器 for(MessageListener listener: listeners) { listener.stop(); } } public class MessageListener implements Runnable { /** * 此监听器监听的topic */ private String topic; /** * 此topic的消费者 */ private List<MessageConsumer> consumers; /** * 绑定线程 */ private Thread t; public MessageListener(String topic, List<MessageConsumer> consumers) { this.topic = topic; this.consumers = consumers; } /** * 将数据反序列化 * @param msg 字符串消息 * @return 消息对象 */ public Object deserialize(String msg) { return translator.deserialize(msg); } public void run() { String msg; Object obj; //从池中抓取一个连接用来监听redis队列 Jedis jedis = jedisPool.getResource(); while(!Thread.interrupted()) { msg = jedis.blpop(1, topic).get(1); obj = deserialize(msg); //收到消息后告知所有消费者 for(MessageConsumer consumer:consumers) { consumer.onMessage(topic, obj); } } jedis.close(); //订阅结束后释放资源 } public void start() { t = new Thread(this); t.start(); } public void stop() { //利用中断打断线程的运行 t.interrupt(); } } }
使用案例
(一)定义好Consumer,注入为容器bean
@Component public class TestConsumer implements MessageConsumer { @Override public void onMessage(String topic, Object message) { System.out.println((SomeObject)message); } @Override public String getTopic() { return "test"; } }
由于Ttranslator会将对象转换好,所以只要将Object强制转换成指定类型即可使用。
(二)全局配置
@Configuration public class TestConfig { @Bean public JedisPool jedisPool() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(5); jedisPoolConfig.setMinIdle(1); return new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 2000, "123456"); } @Bean(value = "rediMQ", initMethod = "start", destroyMethod = "stop") @Autowired public RedisPubSub redisPubSub(List<MessageConsumer> consumers, JedisPool jedisPool) { RedisPubSub redisPubSub = new RedisPubSub(); redisPubSub.setJedisPool(jedisPool); redisPubSub.setConsumerList(consumers); return redisPubSub; } }
@Autowired 配合方法参数的List<MessageConsumer> 就可以得到容器中所有的Consumer。
(三)引入使用
@Service public class SomeService { @Autowired private MessagePublisher publisher; public void someOperation() { publisher.publish("test", new SomeObject()); } }
只需要以MessagePublisher接口的身份引入就可以了。
【编码】封装RedisPubSub工具的更多相关文章
- H264编码 封装成MP4格式 视频流 RTP封包
H264编码 封装成MP4格式 视频流 RTP封包 分类: 多媒体编程 2013-02-20 21:31 3067人阅读 ...
- 转:轻松把玩HttpClient之封装HttpClient工具类(一)(现有网上分享中的最强大的工具类)
搜了一下网络上别人封装的HttpClient,大部分特别简单,有一些看起来比较高级,但是用起来都不怎么好用.调用关系不清楚,结构有点混乱.所以也就萌生了自己封装HttpClient工具类的想法.要做就 ...
- CSV.js – 用于 CSV 解析和编码的 JS 工具库
逗号分隔值(CSV )文件用于以以纯文本的形式存储表格化数据(数字和文本). CSV 文件包含任意数量的记录,通过某种换行符分隔,每条记录由字段,其他一些字符或字符串分隔,最常用的是文字逗号或制表符. ...
- .NET3.5中JSON用法以及封装JsonUtils工具类
.NET3.5中JSON用法以及封装JsonUtils工具类 我们讲到JSON的简单使用,现在我们来研究如何进行封装微软提供的JSON基类,达到更加方便.简单.强大且重用性高的效果. 首先创建一个类 ...
- AJAX编程-封装ajax工具函数
即 Asynchronous [e'sɪŋkrənəs] Javascript And XML,AJAX 不是一门的新的语言,而是对现有技术的综合利用.本质是在HTTP协议的基础上以异步的方式与服务器 ...
- MySQL数据库学习笔记(十一)----DAO设计模式实现数据库的增删改查(进一步封装JDBC工具类)
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- MySQL数据库学习笔记(十)----JDBC事务处理、封装JDBC工具类
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- JAVA中封装JSONUtils工具类及使用
在JAVA中用json-lib-2.3-jdk15.jar包中提供了JSONObject和JSONArray基类,用于JSON的序列化和反序列化的操作.但是我们更习惯将其进一步封装,达到更好的重用. ...
- Char Tools,方便的字符编码转换小工具
工作关系,常有字符编码转换方面的需要,写了这个小工具 Char Tools是一款方便的字符编码转换小工具,基于.Net Framework 2.0 Winform开发 主要功能 URL编码:URLEn ...
随机推荐
- BCB:AnsiString和String的区别
AnsiString和String的区别.使用 本文转自:http://www.bianceng.cn/c/index.htm 16.C/C++语言在CB中的一些特定用法 2)AnsiString是从 ...
- JavaScript -- 语法和数据类型
前戏 前面学了HTML和CSS相关的知识,那JavaScript是做什么的呢?你在网页上看到的那些炫酷的特效都是通过JS来实现的,所以,想要开发一个逼格满满的web页面,JS是必须要会的 什么是Jav ...
- linux环境nginx的安装与使用
因为公司需要需要安装一系列环境,新手上路第一次配的时候什么也不懂在网上找了半天,觉得这篇不错,我在这里顺便记录一下.(原文:https://www.cnblogs.com/wyd168/p/66365 ...
- ECMAScript 继承机制实现
继承机制的实现 要用 ECMAScript 实现继承机制,您可以从要继承的基类入手.所有开发者定义的类都可作为基类.出于安全原因,本地类和宿主类不能作为基类,这样可以防止公用访问编译过的浏览器级的代码 ...
- abaqus二次开发概述
说明 abaqus二次开发概述 导语 用户子程序特点 abaqus用户程序接口与调用方式 abaqus用户子程序分类 常用用户子程序介绍 Refence 说明 本系列文章本人基本没有原创贡献,都是在学 ...
- 初涉2-SAT
2-SAT:有趣的图论模型 什么是2-SAT SAT是适定性(Satisfiability)问题的简称.之所以研究2-sat是因为当k>2时,k-sat问题已经被证明是NPC的了. 2-sat问 ...
- MySQLfailover错误一则
由于公司现有主库要转移到新的主库上,所以,我打算利用MySQLfailover工具的故障转移. 1.开发把程序账号转移到新主库上 2.停止现有主库,使之进行故障转移,转移期间会自动锁表,保持数据一致性 ...
- 解决 mounting /dev/block/mmcblk0p1 on /sdcard failed
http://www.liyu8.com/article/sdcard.htm 之前在recovery下的adb shell执行mount -a总是会有 mount: mouting /dev/blo ...
- PAT Basic 1018
1018 锤子剪刀布 大家应该都会玩“锤子剪刀布”的游戏:两人同时给出手势,胜负规则如图所示: 现给出两人的交锋记录,请统计双方的胜.平.负次数,并且给出双方分别出什么手势的胜算最大. 输入格式: 输 ...
- Disqus 升级到3.0以上版本的评论同步问题
Disqus从2.*升级3.*时,Knowlege Base的文章不显示Disqus评论, 解决方法:在Disqus的Advanced Settings中勾选Render Comments JavaS ...