Springboot+Redis(发布订阅模式)跨多服务器实战
一:redis中发布订阅功能
-
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依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <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配置
1 2 3 4 5 6 7 8 9 10 11 12 | 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发布者发布消息
1 2 3 4 5 6 7 8 | /** * 建立发布者,通过频道发布消息 * @param key 发布者 * @param value 消息 */ public void publish(String key,Object value){ this .redisTemplate.convertAndSend(key,value); }<br><br> |
发送方式:redisUtils.publish(RedisTopicEnums.TOPIC_DISCOVER.getTopic(),message);
4.定义订阅者接收消息器接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * 订阅者接收消息的基类 * @author : 47Gamer * @createdDate : 2020/8/6 * @updatedDate */ public interface Subscriber extends MessageListener { /** * 类型 * @return */ default String getType() { return this .getClass().getSimpleName(); } /** * 通道名称 * @return */ String getTopic(); } |
5.定义不同主题枚举类型,后期增加一个管道,增加一个枚举信息即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /** * 定义不同主题类型 * @author : 47Gamer * @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; } } |
注意:实现多个订阅者,后续增加一个订阅者,只需要多加上一个订阅者类,从而不用改动redis消息 监听容器配置
6.设备健康扫描订阅者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /** * 设备健康扫描的订阅者 * * @author : 47Gamer * @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.配置扫描订阅者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /** * 设备配置变更订阅者 * * @author : 47Gamer * @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配置,消息监听器容器配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | @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; } |
注:编写代码要善于运用设计模式
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具