Springboot+Redis(发布订阅模式)跨多服务器实战
一:redis中发布订阅功能(http://www.redis.cn/commands.html#pubsub)
PSUBSCRIBE pattern [pattern …]:订阅一个或者多个符合pattern格式的频道
PUBLISH channel message:发布消息到chanel中
PUBSUB subcommand [argument [argument …]]:查看订阅与发布系统状态
PUNSUBSCRIBE [pattern [pattern …]]:退订所有符合格式的频道
SUBSCRIBE channel [channel …]:订阅一个或者多个频道
UNSUBSCRIBE [channel [channel …]]:取消订阅频道
二:实战使用reids中的发布订阅模式解决部署在阿里服务与本地后台服务的接口调用不通问题(跨多服务器)
当然也可使用更为强大的消息中间件(RabbitMQ、ActiveMQ、RocketMQ、Kafka、ZeroMQ)
1:redis使用到的maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- springboot1.5版本使用jedis,2.0以上版本使用lettuce,本项目使用jedis,所以需要排除lettuce -->
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<!--
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
-->
</exclusions>
</dependency>
2:application.yml配置
redis:
host: localhost
port: 8379
password: 123456
database: 2
timeout: 50s
# 如果使用的jedis 则将lettuce改成jedis即可
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
3:publisher发布者发布消息
/**
* 建立发布者,通过频道发布消息
* @param key 发布者
* @param value 消息
*/
public void publish(String key,Object value){
this.redisTemplate.convertAndSend(key,value);
}
redisUtils.publish(RedisTopicEnums.TOPIC_DISCOVER.getTopic(),message);
4:第一种实现方法
/**
* redis消息监听器容器
* @param connectionFactory
* @param healthyListenerAdapter 健康扫描消息订阅处理器
* @param settingsListenerAdapter 配置健康扫描消息订阅处理器
* @return
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter healthyListenerAdapter,
MessageListenerAdapter settingsListenerAdapter
) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//设备健康扫描绑定消息订阅处理器
container.addMessageListener(healthyListenerAdapter, new PatternTopic("healthy_topic"));
//设备配置扫描并绑定消息订阅处理器
container.addMessageListener(settingsListenerAdapter, new PatternTopic("settings_topic"));
return container;
} /**
* 设备健康消息订阅处理器,并指定处理方法(利用反射的机制调用消息处理器的业务方法)
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter healthyListenerAdapter(ReceiverRedisMessage receiver) {
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(receiver, "healthy");
return messageListenerAdapter;
} /**
* 设备健康消息订阅处理器,并指定处理方法
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter settingsListenerAdapter(ReceiverRedisMessage receiver) {
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(receiver, "settings");
return messageListenerAdapter;
}
缺点:1:考虑实际运用中可能会订阅多个主题,每加一个主题(Topic)都需要使用container.addMessageListener(listenerAdapter,
new
PatternTopic(
"topic"
));不合理
2:考虑到后期尽量不该变原有代码进行扩展,推荐使用下面第二种方式实现(保证开闭原则)
5:第二种实现方法:定义订阅者接收消息器接口
/**
* 订阅者接收消息的基类
* @author : ywb
* @createdDate : 2020/8/6
* @updatedDate
*/
public interface Subscriber extends MessageListener { /**
* 类型
* @return
*/
default String getType() {
return this.getClass().getSimpleName();
} /**
* 通道名称
* @return
*/
String getTopic(); }
6:定义不同主题枚举类型,后期增加一个管道,增加一个枚举信息即可
/**
* 定义不同主题类型
* @author : ywb
* @createdDate : 2020/8/7
* @updatedDate
*/
public enum RedisTopicEnums { /**
* redis主题名称定义 需要与发布者一致
*
*/
TOPIC_DISCOVERY("topic:discovery", "设备发现变更Topic"), TOPIC_HEALTHY("topic:healthy", "健康扫描的设备Topic"), TOPIC_SETTINGS("topic:settings", "配置扫描变更的设备Topic"), TOPIC_DISCOVER("topic:discover", "发现设备Topic"), ;
/**
* 主题名称
*/
private String topic; /**
* 描述
*/
private String description; RedisTopicEnums(String topic, String description) {
this.topic = topic;
this.description = description;
} public String getTopic() {
return topic;
} public String getDescription() {
return description;
} }
7:实现多个订阅者,后续增加一个订阅者,只需要多加上一个订阅者类,从而不用改动redis消息 监听容器配置
7.1:设备健康扫描订阅者
/**
* 设备健康扫描的订阅者
*
* @author : ywb
* @createdDate : 2020/8/7
* @updatedDate
*/
@Component
@Slf4j
public class HealthySubscriber implements Subscriber { @Autowired
private DeviceService deviceService; @Autowired
private RedisTemplate<String, Object> redisTemplate; @Override
public String getTopic() {
return RedisTopicEnums.TOPIC_HEALTHY.getTopic();
} @Override
public void onMessage(Message message, byte[] pattern) { String deviceIds = (String) redisTemplate.getValueSerializer().deserialize(message.getBody()); log.info(">> 订阅消息,设备健康异常编号:{}", deviceIds); // TODO 这里是收到通道的消息之后执行的方法
String[] split = deviceIds.split(","); Map<String, Set<Integer>> idsMap = TokenSplit.getDeviceIdRegex(split); for (Map.Entry<String, Set<Integer>> stringSetEntry : idsMap.entrySet()) {
DeviceHandle healthyHandle = new DeviceHealthyHandle();
healthyHandle.respondHandle(stringSetEntry.getValue());
} }
}
7.2:配置扫描订阅者
/**
* 设备配置变更订阅者
*
* @author : ywb
* @createdDate : 2020/8/7
* @updatedDate
*/
@Component
@Slf4j
public class SettingsSubscriber implements Subscriber { @Autowired
private RedisTemplate<String, Object> redisTemplate; @Override
public String getTopic() {
return RedisTopicEnums.TOPIC_SETTINGS.getTopic();
} @Override
public void onMessage(Message message, byte[] pattern) { //使用redis convertAndSend发布消息,订阅者获取字符串字节必须要反序列
String deviceIds = (String) redisTemplate.getValueSerializer().deserialize(message.getBody()); log.info(">>订阅消息,设备配置变更编号:{}", deviceIds); // TODO 这里是收到通道的消息之后执行的方法
String[] split = deviceIds.split(","); Map<String, Set<Integer>> idsMap = TokenSplit.getDeviceIdRegex(split); for (Map.Entry<String, Set<Integer>> stringSetEntry : idsMap.entrySet()) {
DeviceScannerHandle scannerHandle = new DeviceScannerHandle();
scannerHandle.respondHandle(stringSetEntry.getValue());
}
}
}
8:redisConfig配置,消息监听器容器配置
@Configuration
public class RedisConfig { /**
* 自定义 redisTemplate<String, Object>
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory); ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// om.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build(), ObjectMapper.DefaultTyping.EVERYTHING);
om.activateDefaultTyping(new LaissezFaireSubTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
om.setTimeZone(TimeZone.getTimeZone("GMT+8"));
// 不转换值为 null 的对象
// om.setSerializationInclusion(JsonInclude.Include.NON_NULL); Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om); // key 采用 string 的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// value 采用 jackson 的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 key 采用 string 的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
// hash 的 value 采用 jackson 的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet(); return template;
} /**
* DependencyDescriptor
* 重点
* 首先判断注入的类型,如果是数组、Collection、Map,则注入的是元素数据,即查找与元素类型相同的Bean,注入到集合中。
* 强调下Map类型,Map的 key 为Bean的 name,value 为 与定义的元素类型相同的Bean。
*将所有相同类型(实现了同一个接口)的Bean,一次性注入到集合类型中,具体实现查看spring源码
*
* 获取Subscriptor接口所有的实现类
* 注入所有实现了接口的Bean
* 将所有的配置消息接收处理类注入进来,那么消息接收处理类里面的注解对象也会注入进来
*/
@Autowired
private transient List<Subscriber> subscriptorList; @Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { //创建一个消息监听对象
RedisMessageListenerContainer container = new RedisMessageListenerContainer(); //将监听对象放入到容器中
container.setConnectionFactory(connectionFactory); if (this.subscriptorList != null && this.subscriptorList.size() > 0) {
for (Subscriber subscriber : this.subscriptorList) { if (subscriber == null || StringUtils.isBlank(subscriber.getTopic())) {
continue;
}
//一个订阅者对应一个主题通道信息
container.addMessageListener(subscriber, new PatternTopic(subscriber.getTopic()));
}
} return container;
}
注:编写代码要善于运用设计模式
Springboot+Redis(发布订阅模式)跨多服务器实战的更多相关文章
- SpringBoot Redis 发布订阅模式 Pub/Sub
SpringBoot Redis 发布订阅模式 Pub/Sub 注意:redis的发布订阅模式不可以将消息进行持久化,订阅者发生网络断开.宕机等可能导致错过消息. Redis命令行下使用发布订阅 pu ...
- redis发布/订阅模式
其实在很多的MQ产品中都存在这样的一个模式,我们常听到的一个例子 就是邮件订阅的场景,什么意思呢,也就是说100个人订阅了你的博客,如果博主发表了文章,那么100个人就会同时收到通知邮件,除了这个 场 ...
- Redis - 发布/订阅模式
Redis 提供了一组命令可以让开发者实现 “发布/订阅” 模式.“发布/订阅” 可以实现进程间的消息传递,其原理是这样的: “发布/订阅” 模式中包含两种角色,分别是发布者和订阅者.订阅者可以订阅一 ...
- 使用EventBus + Redis发布订阅模式提升业务执行性能
前言 最近一直奔波于面试,面了几家公司的研发.有让我受益颇多的面试经验,也有让我感觉浪费时间的面试经历~因为疫情原因,最近宅在家里也没事,就想着使用Redis配合事件总线去实现下具体的业务. 需求 一 ...
- redis 发布/订阅 模式
发布/订阅模式的命令如下: * 进入发布订阅模式的客户端,不能执行除发布订阅模式以上命令的其他命令,否则出错.
- 使用EventBus + Redis发布订阅模式提升业务执行性能(下)
前言 上一篇博客上已经实现了使用EventBus对具体事件行为的分发处理,某种程度上也算是基于事件驱动思想编程了.但是如上篇博客结尾处一样,我们源码的执行效率依然达不到心里预期.在下单流程里我们明显可 ...
- 把酒言欢话聊天,基于Vue3.0+Tornado6.1+Redis发布订阅(pubsub)模式打造异步非阻塞(aioredis)实时(websocket)通信聊天系统
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_202 "表达欲"是人类成长史上的强大"源动力",恩格斯早就直截了当地指出,处在蒙昧时代即低 ...
- Redis 发布订阅,小功能大用处,真没那么废材!
今天小黑哥来跟大家介绍一下 Redis 发布/订阅功能. 也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的,没关系小黑哥先来举个例子. 假设我们有这么一个业务场景,在网站下单支付以后,需要 ...
- springboot集成redis实现消息发布订阅模式-双通道(跨多服务器)
基础配置参考https://blog.csdn.net/llll234/article/details/80966952 查看了基础配置那么会遇到一下几个问题: 1.实际应用中可能会订阅多个通道,而一 ...
随机推荐
- leetcode刷题-79单词搜索
题目 给定一个二维网格和一个单词,找出该单词是否存在于网格中. 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格.同一个单元格内的字母不允许被重复 ...
- C#开发PACS医学影像处理系统(三):界面布局之工具栏
工具栏布局采用WPF中Grid作为容器,按钮采用自定义样式和图标,并采用Separator分割线: XAML设计器代码: 其中 Style="{StaticResource ButtonS ...
- Python3实现zip分卷压缩
Python实现zip分卷压缩 使用 zipfile 库 查看 官方中文文档 利用 Python 压缩 ZIP 文件,我们第一反应是使用 zipfile 库,然而,它的官方文档中却明确标注" ...
- Robotframework自动化4-基础关键字介绍1
前言 上一节已经介绍了APP的启动,那我们就会看到我们引用了一些关键字,对于AppiumLibrary都有哪些常用的关键呢,这一节主要介绍这一部分. AppiumLibrary 常用关键字介绍 1.关 ...
- Linux实战(19):Shell交互式read 用法
read 用法有好几种,我在实战过程中用到了 -p,记一笔以防不用忘记了. 实例 #!/bin/bash echo "检测IP是否被占用" while read -p " ...
- RXJAVA之聚合操作
concat 按顺序连接多个Observables.需要注意的是Observable.concat(a,b)等价于a.concatWith(b). startWith 在数据序列的开头增加一项数据.s ...
- 使用Golang的singleflight防止缓存击穿
背景 在使用缓存时,容易发生缓存击穿. 缓存击穿:一个存在的key,在缓存过期的瞬间,同时有大量的请求过来,造成所有请求都去读dB,这些请求都会击穿到DB,造成瞬时DB请求量大.压力骤增. singl ...
- java面试题(一)
1.面向对象的特征有哪些方面? - 1 - 2.访问修饰符public,private,protected,以及不写(默认)时的区别? - 1 - 3.String 是最基本的数据类型吗? - 1 - ...
- 栈的Java实现-分别使用数组和链表
栈是非常重要的数据结构,栈具有后进先出的特点. 在JVM内部,每个线程维护一个栈,对于每个方法调用,入栈一个元素,成为栈帧,当方法执行完成后,对应的栈帧出栈. 栈帧中,也包含一个栈,称为操作数栈. 一 ...
- 20行代码实现,使用Tarjan算法求解强连通分量
今天是算法数据结构专题的第36篇文章,我们一起来继续聊聊强连通分量分解的算法. 在上一篇文章当中我们分享了强连通分量分解的一个经典算法Kosaraju算法,它的核心原理是通过将图翻转,以及两次递归来实 ...