【编码】封装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 ...
随机推荐
- Hello World投票以太坊Dapp教程-Part1
参考资料:https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0 ...
- java socket domain name 使用域名.
java 的 socket 依赖了 nameService. 引擎模式. 使得 socket tcp 层 具有了上层业务的能力 (应用层) Socket socket=new Socket(&quo ...
- css3的border-radius属性使用方法
1.border-radius可以包含两个参数值,第一个水平圆角半径,第二个为垂直半径,并且两个参数值用“/”分开. 2.border-radius:设置一个值为四个角都相同,两个值为左上和右下相同, ...
- mysql函数总结
MySQL函数 MySQL数据库提供了很多函数包括: 数学函数:字符串函数:日期和时间函数:条件判断函数:系统信息函数:加密函数:格式化函数: 一.数学函数 数学函数主要用于处理数字,包括整型.浮点数 ...
- 1、初学探讨PYTHON的itchat和wxpy两库
最近好奇学习了python,觉得简单明了,但是最头疼的就是调整空格和调试吧,的确调试不如C#使用visual studio 方便,都是使用print()来调试.也许因为我是菜鸟,如果大家还有更好的方法 ...
- linux下C++的多线程编程
1. 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程(proces ...
- mysql主从复制延时判断+脚本检查
在生产环境中,主从复制常常会有复制延迟的现象,主要是master是并发的写,而slave是单线程的应用relay log,所以会出现复制延时,在MySQL 5.6版本中有了基于库的多线程复制.还有Ma ...
- as eclipse 对比
eclipse目录结构 src:存放prj源码 gen:自动生成文件,R类存放资源文件的唯一id android+版本:项目jar包 assets:res存放资源文件,不会被gen生成资源id,同过a ...
- tomcat 修改默认端口8080 为 80端口
首先,找到你的安装目录,如图: 打开server.xml文件,找到8080,如图: 将 8080 改成你想要的端口,如 80 即可.改完后,记得要重启tomcat! 将端口改成 80 后,访问就不需 ...
- javamail腾讯企业邮箱发送邮件
此代码用的jar文件:mail.jar(1.4.5版本); 如果jdk用的是1.8版本会出现SSL错误:这个问题是jdk导致的,jdk1.8里面有一个jce的包,安全性机制导致的访问https会报错, ...