补习系列(13)-springboot redis 与发布订阅

雨燕双飞 提交于 2020-12-01 01:48:14

一、订阅发布


订阅发布是一种常见的设计模式,常见于消息系统的场景。 如下面的图:

[来自百科] 

消息发布者是消息载体的生产者,其通过某些主题来向调度中心发送消息; 而消息订阅者会事先向调度中心订阅其"感兴趣"的主题,随后会获得新消息。 

在这里,调度中心是一个负责消息控制中转的逻辑实体,可以是消息队列如ActiveMQ,也可以是Web服务等等。


常见应用

  • 微博,每个用户的粉丝都是该用户的订阅者,当用户发完微博,所有粉丝都将收到他的动态;

  • 新闻,资讯站点通常有多个频道,每个频道就是一个主题,用户可以通过主题来做订阅(如RSS),这样当新闻发布时,订阅者可以获得更新。



二、Redis 与订阅发布

Redis 支持 (pub/sub) 的订阅发布能力,客户端可以通过channel(频道)来实现消息的发布及接收。


1. 客户端通过 SUBSCRIBE 命令订阅 channel


2. 客户端通过PUBLISH 命令向channel 发送消息;

而后,订阅 channel的客户端可实时收到消息。

除了简单的SUBSCRIBE/PUBLISH命令之外,Redis还支持订阅某一个模式的主题(正则表达式), 如下:

  
    
  
  
  1. PSUBSCRIBE  /topic/cars/*


于是,我们可以利用这点实现相对复杂的订阅能力,比如:

  • 在电商平台中订阅多个品类的商品促销信息;

  • 智能家居场景,APP可以订阅所有房间的设备消息。 ...


尽管如此,Redis pub/sub 机制存在一些缺点:

  • 消息无法持久化,存在丢失风险;

  • 没有类似 RabbitMQ的ACK机制;

  • 由于是广播机制,无法通过添加worker 提升消费能力;

因此,Redis 的订阅发布建议用于实时且可靠性要求不高的场景。


三、SpringBoot 与订阅发布

接下来,看一下SpringBoot 怎么实现订阅发布的功能。


spring-boot-starter-data-redis 帮我们实现了Jedis的引入

pom 依赖如下:

  
    
  
  
  1. <!-- redis -->

  2.  <dependency>

  3.   <groupId>org.springframework.boot</groupId>

  4.   <artifactId>spring-boot-starter-data-redis</artifactId>

  5.   <version>${spring-boot.version}</version>

  6.  </dependency>


在 application.properties 中指定配置

  
    
  
  
  1. # redis 连接配置

  2. spring.redis.database=0

  3. spring.redis.host=127.0.0.1

  4. spring.redis.password=

  5. spring.redis.port=6379

  6. spring.redis.ssl=false

  7. # 连接池最大数

  8. spring.redis.pool.max-active=10

  9. # 空闲连接最大数

  10. spring.redis.pool.max-idle=10

  11. # 获取连接最大等待时间(s)

  12. spring.redis.pool.max-wait=600000


A. 消息模型

消息模型描述了订阅发布的数据对象,这要求生产者与消费者都能理解 

以下面的POJO为例:

  
    
  
  
  1.    public static class SimpleMessage {

  2.        private String publisher;

  3.        private String content;

  4.        private Date createTime;


在SimpleMessage类中,我们声明了几个字段:

字段名 说明
publisher 发布者
content 文本内容
createTime 创建时间


B. 序列化

如下的代码采用了JSON 作为序列化方式:

  
    
  
  
  1. @Configuration

  2. public class RedisConfig {

  3.    private static final Logger logger = LoggerFactory.getLogger(RedisConfig.class);

  4.    /**

  5.     * 序列化定制

  6.     *

  7.     * @return

  8.     */

  9.    @Bean

  10.    public Jackson2JsonRedisSerializer<Object> jackson2JsonSerializer() {

  11.        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(

  12.                Object.class);

  13.        // 初始化objectmapper

  14.        ObjectMapper mapper = new ObjectMapper();

  15.        mapper.setSerializationInclusion(Include.NON_NULL);

  16.        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

  17.        jackson2JsonRedisSerializer.setObjectMapper(mapper);

  18.        return jackson2JsonRedisSerializer;

  19.    }

  20.    /**

  21.     * 操作模板

  22.     *

  23.     * @param connectionFactory

  24.     * @param jackson2JsonRedisSerializer

  25.     * @return

  26.     */

  27.    @Bean

  28.    public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory connectionFactory,

  29.            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer) {

  30.        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();

  31.        template.setConnectionFactory(connectionFactory);

  32.        // 设置key/hashkey序列化

  33.        RedisSerializer<String> stringSerializer = new StringRedisSerializer();

  34.        template.setKeySerializer(stringSerializer);

  35.        template.setHashKeySerializer(stringSerializer);

  36.        // 设置值序列化

  37.        template.setValueSerializer(jackson2JsonRedisSerializer);

  38.        template.setHashValueSerializer(jackson2JsonRedisSerializer);

  39.        template.afterPropertiesSet();

  40.        return template;

  41.    }


C. 发布消息

消息发布,需要先指定一个ChannelTopic对象,随后通过RedisTemplate方法操作。

  
    
  
  
  1. @Service

  2. public class RedisPubSub {  

  3.    private static final Logger logger = LoggerFactory.getLogger(RedisPubSub.class);

  4.    @Autowired

  5.    private RedisTemplate<String, Object> redisTemplate;

  6.    private ChannelTopic topic = new ChannelTopic("/redis/pubsub");

  7.    @Scheduled(initialDelay = 5000, fixedDelay = 10000)

  8.    private void schedule() {

  9.        logger.info("publish message");

  10.        publish("admin", "hey you must go now!");

  11.    }

  12.    /**

  13.     * 推送消息

  14.     *

  15.     * @param publisher

  16.     * @param message

  17.     */

  18.    public void publish(String publisher, String content) {

  19.        logger.info("message send {} by {}", content, publisher);

  20.        SimpleMessage pushMsg = new SimpleMessage();

  21.        pushMsg.setContent(content);

  22.        pushMsg.setCreateTime(new Date());

  23.        pushMsg.setPublisher(publisher);

  24.        redisTemplate.convertAndSend(topic.getTopic(), pushMsg);

  25.    }

上述代码使用一个定时器(@Schedule)来做发布,为了保证运行需要在主类中启用定时器注解:

  
    
  
  
  1. @EnableScheduling

  2. @SpringBootApplication

  3. public class BootSampleRedis{

  4. ...

  5. }


D. 接收消息

定义一个消息接收处理的Bean:

  
    
  
  
  1.    @Component

  2.    public static class MessageSubscriber {

  3.        public void onMessage(SimpleMessage message, String pattern) {

  4.            logger.info("topic {} received {} ", pattern, JsonUtil.toJson(message));

  5.        }

  6.    }


接下来,利用 MessageListenerAdapter 可将消息通知到Bean方法:

  
    
  
  
  1.       /**

  2.         * 消息监听器,使用MessageAdapter可实现自动化解码及方法代理

  3.         *

  4.         * @return

  5.         */

  6.        @Bean

  7.        public MessageListenerAdapter listener(Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer,

  8.                MessageSubscriber subscriber) {

  9.            MessageListenerAdapter adapter = new MessageListenerAdapter(subscriber, "onMessage");

  10.            adapter.setSerializer(jackson2JsonRedisSerializer);

  11.            adapter.afterPropertiesSet();

  12.            return adapter;

  13.        }


最后,关联到消息发布的Topic:

  
    
  
  
  1.        /**

  2.         * 将订阅器绑定到容器

  3.         *

  4.         * @param connectionFactory

  5.         * @param listenerAdapter

  6.         * @return

  7.         */

  8.        @Bean

  9.        public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,

  10.                MessageListenerAdapter listener) {

  11.            RedisMessageListenerContainer container = new RedisMessageListenerContainer();

  12.            container.setConnectionFactory(connectionFactory);

  13.            container.addMessageListener(listener, new PatternTopic("/redis/*"));

  14.            return container;

  15.        }


运行结果

启动程序,从控制台可输出:

  
    
  
  
  1. .RedisPubSub : publish message

  2. .RedisPubSub : message send hey you must go now! by admin

  3. .RedisPubSub : topic /redis/* received {"publisher":"admin","content":"hey you must go now!","createTime":1543418694007}

这样,我们便完成了订阅发布功能。


小结

消息订阅发布是分布式系统中的常用手段,也经常用来实现系统解耦、性能优化等目的; 当前小节结合SpringBoot 演示了 Redis订阅发布(pub/sub)的实现,在部分场景下可以参考使用。 欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容^-^




相关文章推荐:

补习系列(12)-springboot 与邮件发送

补习系列(11)-springboot 文件上传原理

补习系列(10)-springboot 之配置读取

补习系列(9)- springboot 定时器,你用对了吗

补习系列(8)-springboot 单元测试之道

补习系列(7)-springboot 拦截器五大姿势

补习系列(6)- springboot 整合 shiro 一指禅

补习系列(5)-springboot restful实战


本文分享自微信公众号 - 美码师(gracebuilding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!