Redis 的发布和订阅

333 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

Redis 的发布和订阅

什么是发布和订阅

  • Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
  • Redis 客户端可以订阅任意数量的频道。

Redis 的发布和订阅

  • Redis 发布订阅的消息不会被持久化,所以新订阅的客户端将收不到历史消息。
  • 消息及时通讯(redis不像中间件一样,不会对发布的消息进行缓存,确认,重试等机制)
  • 客户端可以订阅频道如下图:

图片.png

  • 当给这个频道发布消息后,消息就会发送给订阅的客户端:

图片.png

发布订阅命令行实现

打开一个客户端订阅 channel1:

  • SUBSCRIBE channel [channel ...]
  • 订阅给定的一个或多个频道的信息。
  • 返回值:接收到的信息(请参见下面的代码说明)。
redis> SUBSCRIBE channel1 channel2
Reading messages... (press Ctrl-C to quit)
1) "subscribe" # 返回值的类型:显示订阅成功
2) "channel1"       # 订阅的频道名字
3) (integer) 1 # 目前已订阅的频道数量
 
1) "subscribe"
2) "channel2"
3) (integer) 2

图片.png

打开另一个客户端,给 channel1 发布消息 hello channel:

  • PUBLISH channel message
  • 将信息 message 发送到指定的频道 channel 。
  • 返回值:接收到信息 message 的订阅者数量。
# 对没有订阅者的频道发送信息
redis> publish channel1 "channel1"
(integer) 0

# 向有一个订阅者的频道发送信息
redis> publish channel2 "channel2"
(integer) 1

# 向有多个订阅者的频道发送信息
redis> publish channel2 "channel3"
(integer) 3

图片.png

打开第一个客户端可以看到发送的消息:

图片.png

订阅一个或多个符合给定模式的频道

  • 订阅一个或多个符合给定模式的频道。

  • 每个模式以 * 作为匹配符,比如 it* 匹配所有以 it 开头的频道( it.news 、 it.blog 、it.tweets 等等), news.* 匹配所有以 news. 开头的频道( news.it 、 news.global.today 等等),诸如此类。

  • 订阅

redis> psubscribe news.* tweet.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"          # 返回值的类型:显示订阅成功
2) "news.*"              # 订阅的模式
3) (integer) 1           # 目前已订阅的模式的数量
 
1) "psubscribe"
2) "tweet.*"
3) (integer) 2
  • 收到的消息
1) "pmessage"            # 返回值的类型:信息
2) "news.*"              # 信息匹配的模式
3) "news.it"             # 信息本身的目标频道
4) "hello hello"         # 信息的内容
 
1) "pmessage"
2) "tweet.*"
3) "tweet.huangz"
4) "hello"

适用场景

  1. 优惠券到期到期失效
  2. 微信粉丝关注公众号
  3. 配置字典加载到缓存,订阅一个频道,当配置更新时动态刷新缓存

RedisTemplate 操作 发布订阅

yaml

# Redis服务器地址
spring:
  redis:
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis数据库索引(默认为0)
    database: 0
    # Redis数据库密码
    password:
    # 连接超时时间(毫秒)
    timeout: 1800000
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 20
        # 最大阻塞等待时间(负数表示没限制)
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 5
        # 连接池中的最小空闲连接
        min-idle: 0

消息对象

@Data
public class TaskSynData implements Serializable {

    private static final long serialVersionUID = -1681130765215741845L;

    /**
     * 任务名字
     */
    private String name;

    /**
     * 任务类型
     */
    private String type;

订阅者

多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。

MailReceiver

@Component
public class MailReceiver {

    public void receiveMessage(TaskSynData taskSynData ) {
        System.out.println("mail 收到消息为:" + taskSynData);
    }
}

SmsReceiver

@Component
public class SmsReceiver {

    public void receiveMessage(TaskSynData taskSynData ) {
        System.out.println("sms 收到消息为:" + taskSynData);
    }
}

配置

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }


    // ##### 上方为redis通用配置
    /**
     * 绑定消息监听者和接收监听的方法,必须要注入这个监听器,不然会报错
     */
    @Bean
    public MessageListenerAdapter mailAdapter(MailReceiver mailReceiver) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(mailReceiver, "receiveMessage");
        //配置序列化对象
        Jackson2JsonRedisSerializer<TaskSynData> serializer = new Jackson2JsonRedisSerializer<>(TaskSynData.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        messageListenerAdapter.setSerializer(serializer);
        return messageListenerAdapter;
    }

    @Bean
    public MessageListenerAdapter smsAdapter(SmsReceiver smsReceiver) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(smsReceiver, "receiveMessage");
        //配置序列化对象
        Jackson2JsonRedisSerializer<TaskSynData> serializer = new Jackson2JsonRedisSerializer<>(TaskSynData.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        messageListenerAdapter.setSerializer(serializer);
        return messageListenerAdapter;
    }

    /**
     * Redis消息监听
     *
     * @param redisConnectionFactory redis连接工厂
     * @return 结果
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
                                                   MessageListenerAdapter mailAdapter,
                                                   MessageListenerAdapter smsAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);

        // 各自订阅自己的频道
        container.addMessageListener(mailAdapter, new PatternTopic("mail"));
        container.addMessageListener(smsAdapter, new PatternTopic("sms"));
        // 订阅一个公共频道
        container.addMessageListener(mailAdapter, new PatternTopic("commen"));
        container.addMessageListener(smsAdapter, new PatternTopic("commen"));

        return container;
    }
}

主要的配置可以分解为:

  1. 定义MessageListenerAdapter (规定消息的处理方式)

new MessageListenerAdapter(mailReceiver, "receiveMessage"); 消息会被传递给,mailReceiverreceiveMessage 方法.

@Bean
    public MessageListenerAdapter mailAdapter(MailReceiver mailReceiver) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(mailReceiver, "receiveMessage");
        //配置序列化对象
        Jackson2JsonRedisSerializer<TaskSynData> serializer = new Jackson2JsonRedisSerializer<>(TaskSynData.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        messageListenerAdapter.setSerializer(serializer);
        return messageListenerAdapter;
    }

    @Bean
    public MessageListenerAdapter smsAdapter(SmsReceiver smsReceiver) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(smsReceiver, "receiveMessage");
        //配置序列化对象
        Jackson2JsonRedisSerializer<TaskSynData> serializer = new Jackson2JsonRedisSerializer<>(TaskSynData.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        messageListenerAdapter.setSerializer(serializer);
        return messageListenerAdapter;
    }
  1. 将订阅者订阅到频道
@Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
                                                   MessageListenerAdapter mailAdapter,
                                                   MessageListenerAdapter smsAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);

        // 各自订阅自己的频道
        container.addMessageListener(mailAdapter, new PatternTopic("mail"));
        container.addMessageListener(smsAdapter, new PatternTopic("sms"));
        // 订阅一个公共频道
        container.addMessageListener(mailAdapter, new PatternTopic("commen"));
        container.addMessageListener(smsAdapter, new PatternTopic("commen"));

        return container;
    }

发送消息

@SpringBootTest
@Slf4j
class RedisReceiverTest {

    @Resource
    private RedisTemplate redisTemplate;

    public String redisSend(int i,String channel) {
        TaskSynData taskSynData = new TaskSynData();
        taskSynData.setType("type");
        taskSynData.setName("测试-"+i);
        redisTemplate.convertAndSend(channel, taskSynData);
        return "发送成功";
    }
}

动态添加发布者和订阅者

失败:目前没找到合适的api,不确定是否支持这个功能

@Test
public void test2(){
    log.info("发送config配置的频道-{}",redisSend(0,"sms"));

    // 订阅频道
    redisMessageListenerContainer.addMessageListener(buildMessageListenerAdapter(new NewReceiver()),new PatternTopic("new"));
    
    log.info("发送新配置的频道-{}",redisSend(1,"new"));
}

public MessageListenerAdapter buildMessageListenerAdapter(NewReceiver newReceiver){
    MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(newReceiver, "receiveMessage");
    //配置序列化对象
    Jackson2JsonRedisSerializer<TaskSynData> serializer = new Jackson2JsonRedisSerializer<>(TaskSynData.class);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    serializer.setObjectMapper(objectMapper);
    messageListenerAdapter.setSerializer(serializer);
    return messageListenerAdapter;
}

接收消息报错

2022-10-12 17:16:22.413  INFO 101210 --- [           main] org.example.config.RedisReceiverTest     : 发送config配置的频道-发送成功
2022-10-12 17:16:22.428  INFO 101210 --- [           main] org.example.config.RedisReceiverTest     : 发送新配置的频道-发送成功
sms 收到消息为:TaskSynData(name=测试-0, type=type)
2022-10-12 17:16:22.470 ERROR 101210 --- [enerContainer-4] o.s.d.r.l.a.MessageListenerAdapter       : Listener execution failed