Redis消息:发布订阅 (pub/sub)——使用篇

Java 8

Spring Boot 2.7.3

Redis version 3.0.504 (Windows 版本)

--ben发布于博客园

 

Redis也可以做消息通知——发送消息PUB、接收消息SUB。

相对于RabbitMQ、Kafka等专门的消息系统,其消息通知功能较为简单。

本文介绍使用Redis消息通知的几种方式。

 

1、命令行方式

Windows 10

--

启动Redis服务器;

使用 redis-cli 命令访问 Redis服务器:终端#1ben发布于博客园

使用 subscribe 命令订阅一个频道:开始监听

127.0.0.1:6379> subscribe mqredis0920
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mqredis0920"
3) (integer) 1

 

使用 redis-cli 命令访问 Redis服务器:终端#2

使用 publish 命令发布多条消息:

127.0.0.1:6379> publish mqredis0920 123
(integer) 1
127.0.0.1:6379> publish mqredis0920 abc
(integer) 1
127.0.0.1:6379> publish mqredis0920 "this is a test"
(integer) 1
127.0.0.1:6379> publish mqredis0920 "this is a test 1138"
(integer) 1
127.0.0.1:6379>

执行publish命令后,返回了 整数 1。

 

终端#1 收到了 终端#2 发送的消息:ben发布于博客园

127.0.0.1:6379> subscribe mqredis0920
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mqredis0920"
3) (integer) 1
1) "message"
2) "mqredis0920"
3) "123"
1) "message"
2) "mqredis0920"
3) "abc"
1) "message"
2) "mqredis0920"
3) "this is a test"
1) "message"
2) "mqredis0920"
3) "this is a test 1138"

 

在终端#2 给 没有被订阅的频道 发送一条消息:返回 0

127.0.0.1:6379> publish mqredis0920x "this is a test 1138"
(integer) 0
127.0.0.1:6379>

 

subscribe、publish 命令帮助信息:

subscribe 可以一次订阅多个频道。

127.0.0.1:6379> help subscribe

  SUBSCRIBE channel [channel ...]
  summary: Listen for messages published to the given channels
  since: 2.0.0
  group: pubsub

127.0.0.1:6379> help publish

  PUBLISH channel message
  summary: Post a message to a channel
  since: 2.0.0
  group: pubsub

127.0.0.1:6379>

 

从 参考资料#2 可知,除了订阅频道,还可以订阅模式——使用 PSUBSCRIBE 命令。

127.0.0.1:6379> help psubscribe

  PSUBSCRIBE pattern [pattern ...]
  summary: Listen for messages published to channels matching the given patterns
  since: 2.0.0
  group: pubsub

 

可以使用下面的命令查看 Redis 的 发布订阅的所有命令:

127.0.0.1:6379> help @pubsub

  PSUBSCRIBE pattern [pattern ...]
  summary: Listen for messages published to channels matching the given patterns
  since: 2.0.0

  PUBLISH channel message
  summary: Post a message to a channel
  since: 2.0.0

  PUBSUB subcommand [argument [argument ...]]
  summary: Inspect the state of the Pub/Sub subsystem
  since: 2.8.0

  PUNSUBSCRIBE [pattern [pattern ...]]
  summary: Stop listening for messages posted to channels matching the given patterns
  since: 2.0.0

  SUBSCRIBE channel [channel ...]
  summary: Listen for messages published to the given channels
  since: 2.0.0

  UNSUBSCRIBE [channel [channel ...]]
  summary: Stop listening for messages posted to the given channels
  since: 2.0.0

127.0.0.1:6379>

注意,不同版本的Redis会有不同。ben发布于博客园

 

2、Spring Boot方式(1)

官方文档 Spring Data Redis 之 Redis Messaging (Pub/Sub)ben发布于博客园

https://docs.spring.io/spring-data/redis/docs/current/reference/html/#pubsub

 

本节展示使用 RedisTemplate 发送消息,使用 RedisConnection 订阅消息。

 

项目依赖:

spring-boot-starter-data-redis

lombok

 

下面建立 1个发送者,2个消费者。ben发布于博客园

// 启动类

@Component
@RequiredArgsConstructor
@Slf4j
public class Redis1Runner implements ApplicationRunner {
	
    // redisTemplate 注入方式1
	@Resource(name = "redisTemplate")
	private RedisTemplate<String, Object> redisTemplate;
	
	private final AsyncService asyncService;

	@Override
	public void run(ApplicationArguments args) throws Exception {
		log.info("start: redisTemplate={}", redisTemplate);
		log.info("start: redisTemplate.serializer={}, {}", redisTemplate.getKeySerializer(), redisTemplate.getValueSerializer());
		
		// 设置序列化器
		redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
		redisTemplate.setValueSerializer(StringRedisSerializer.UTF_8);
		
		log.info("start: redisTemplate.serializer={}, {}", redisTemplate.getKeySerializer(), redisTemplate.getValueSerializer());
		
		// 订阅:第1个消费者
		asyncService.subscribeFromRedis();
		
		// 订阅:第2个消费者
		asyncService.subscribeFromRedis();
		
		// 发布者
		asyncService.publishToRedis();
		
		log.info("end");
	}

}

@Service
@RequiredArgsConstructor // redisTemplate 注入方式2
@Async // 异步执行
@Slf4j
public class AsyncService {
	
	private final RedisTemplate redisTemplate;

    // 发布消息:
	public void publishToRedis() {
		log.info("publishToRedis start: redisTemplate={}", redisTemplate);
		
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
        // 5条消息
		IntStream.range(0, 5).forEach(i->{
			redisTemplate.convertAndSend(RedisConstants.CHANNEL_0, "test" + i);
		});
		log.info("sent 1");
	}
	
    // 订阅想消息
	public void subscribeFromRedis() {
		log.info("subscribeFromRedis start: redisTemplate={}", redisTemplate);
		
		RedisConnectionFactory rcf = redisTemplate.getConnectionFactory();
		RedisConnection rc = rcf.getConnection();
		log.info("rcf={}, conn={}", rcf, rc);
		
		rc.subscribe(new MessageListener() {
			
			@Override
			public void onMessage(Message message, byte[] pattern) {
				log.info("message={}, {}", new String(message.getBody()), new String(message.getChannel()));
			}
		}, RedisConstants.CHANNEL_0.getBytes());
		
		log.info("subscribed.");
	}
	
}

 

启动后的日志:

两个消费者都收到了 5条消息,按顺序收到各条消息。ben发布于博客园

 

(高层的)RedisTemplate 可以发送消息,但只有一个发方法,而且是基于频道的:ben发布于博客园

public void convertAndSend(String channel, Object message) {
    ...
}

也可以使用 底层的RedisConnection 对象来发送消息:extends RedisCommands >> extends RedisPubSubCommands

public interface RedisConnection extends RedisCommands, AutoCloseable {
    ...
}

public interface RedisCommands extends RedisKeyCommands, RedisStringCommands, RedisListCommands, RedisSetCommands,
		RedisZSetCommands, RedisHashCommands, RedisTxCommands, RedisPubSubCommands, RedisConnectionCommands,
		RedisServerCommands, RedisStreamCommands, RedisScriptingCommands, RedisGeoCommands, RedisHyperLogLogCommands {
	@Nullable
	Object execute(String command, byte[]... args);
}

public interface RedisPubSubCommands {
    boolean isSubscribed();
    Subscription getSubscription();
    Long publish(byte[] channel, byte[] message);
    void subscribe(MessageListener listener, byte[]... channels);
    void pSubscribe(MessageListener listener, byte[]... patterns);
}

 

3、Spring Boot方式(2):RedisMessageListenerContainer

根据官网的介绍,还可以使用 RedisMessageListenerContainer 来管理消息订阅。

需要结合 MessageListener 接口使用。ben发布于博客园

代码如下:

1)消息监听器

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component // 生成一个Bean
@Slf4j
public class SubChannelOne implements MessageListener {

	@Override
	public void onMessage(Message message, byte[] pattern) {
		String chl = new String(message.getChannel());
		String msg = new String(message.getBody());
		String pstr = "NULL";
		if (pattern != null) {
			log.info("pattern.length={}", pattern.length);
			pstr = new String(pattern);
		}
		log.info("SubChannelOne chl={}, msg={}, pstr={}", chl, msg, pstr);
	}

}

2)消息监听器容器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

import lombok.extern.slf4j.Slf4j;

/**
 * Redis订阅发布配置
 * @author ben
 * @date 2022年9月21日 上午9:29:08
 * @since
 */
@Configuration
@Slf4j
public class RedisPubSubConfig {
	
	public static final String CHANNEL_ONE = "channelOne";

	@Bean
	public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
			SubChannelOne subChannelOne) {
		log.info("RedisPubSubConfig factory={}, subChannelOne={}", factory, subChannelOne);
		
		RedisMessageListenerContainer  container = new RedisMessageListenerContainer();
		
		container.setConnectionFactory(factory);
		
		container.addMessageListener(subChannelOne, ChannelTopic.of(CHANNEL_ONE));
		
		return container;
	}
	
}

启动后生成了 名为 redisMessageListenerContainer、subChannelOne 的Bean。

入口服务启动后,休眠 main线程 1分钟;

public class RedisdemoApplication {

	public static void main(String[] args) throws InterruptedException {
		ConfigurableApplicationContext ctx = SpringApplication.run(RedisdemoApplication.class, args);
		
		log.info("----STARTED----");
		
		TimeUnit.SECONDS.sleep(60); // 休眠1分钟,期间测试发送Redis消息
		
		log.info("----END----");
	}
}

 

日志输出:ben发布于博客园

c.b.redisdemo.pubsub2.RedisPubSubConfig  : RedisPubSubConfig 
factory=org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory@d176a31, 
subChannelOne=com.ben.redisdemo.pubsub2.SubChannelOne@3a91d146

发送和接收消息:正常

ben发布于博客园

3.1、新建MessageListener对象给container使用

除了上面自动注入 SubChannelOne 的方式,还可以新建 SubChannelOne对象 后给 container使用:

	@Bean
	public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory) {
		SubChannelOne subChannelOne = new SubChannelOne(); // 新建对象
		
		log.info("RedisPubSubConfig factory={}, subChannelOne={}", factory, subChannelOne);
		
		RedisMessageListenerContainer  container = new RedisMessageListenerContainer();
		
		container.setConnectionFactory(factory);
		
		container.addMessageListener(subChannelOne, ChannelTopic.of(CHANNEL_ONE));
		
		return container;
	}

 

3.2、使用  MessageListenerAdapter 封装 listener

消息接收正常。

	@Bean
	public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory) {
		SubChannelOne subChannelOne = new SubChannelOne();
		MessageListenerAdapter adapter = new MessageListenerAdapter(subChannelOne, "onMessage");
		
		log.info("RedisPubSubConfig factory={}, subChannelOne={}", factory, subChannelOne);
		log.info("RedisPubSubConfig adapter={}", adapter);
		
		RedisMessageListenerContainer  container = new RedisMessageListenerContainer();
		
		container.setConnectionFactory(factory);
		
		container.addMessageListener(adapter, ChannelTopic.of(CHANNEL_ONE));
		
		return container;
	}

ben发布于博客园

3.3、给RedisMessageListenerContainer配置线程池

在前面的消息接收中,使用的线程包含“”。其实,container可以配置自己的线程池。

	@Bean
	public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory) {
		SubChannelOne subChannelOne = new SubChannelOne();
		MessageListenerAdapter adapter = new MessageListenerAdapter(subChannelOne, "onMessage");
		
		log.info("RedisPubSubConfig factory={}, subChannelOne={}", factory, subChannelOne);
		log.info("RedisPubSubConfig adapter={}", adapter);
		
		RedisMessageListenerContainer  container = new RedisMessageListenerContainer();
		
		container.setConnectionFactory(factory);
		
		// 配置线程池
		TaskExecutor taskExecutor = makeNewTaskExecutor();
		container.setTaskExecutor(taskExecutor);
		
		container.addMessageListener(adapter, ChannelTopic.of(CHANNEL_ONE));
		
		return container;
	}
	
	private TaskExecutor makeNewTaskExecutor() {
		// org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		
		// 必须,否则,
		// java.lang.IllegalStateException: ThreadPoolTaskExecutor not initialized
		executor.initialize();
		
		executor.setCorePoolSize(20);
		executor.setQueueCapacity(100);
		executor.setMaxPoolSize(50);
		
		// 线程名前缀
		executor.setThreadNamePrefix("redis-msg-");
		
		return executor;
	}

上面的线程池的线程前缀为 “redis-msg-”。测试结果:配置成功

RedisMessageListenerContainer 类结构:还可以有其它使用方式,比如,动态修改消息监听

MessageListener、MessageListenerAdapter的类机构:

ben发布于博客园

参考资料

1、SpringBoot整合Redis实现发布订阅功能实践

https://www.cnblogs.com/kendoziyu/p/15802698.html

2、菜鸟教程:Redis 发布订阅

https://www.runoob.com/redis/redis-pub-sub.html

3、官方文档 Spring Data Redis 之 Redis Messaging (Pub/Sub)

https://docs.spring.io/spring-data/redis/docs/current/reference/html/#pubsub

4、Redis(spring data redis) 发布订阅 pub/sub

https://blog.csdn.net/top_explore/article/details/94356245

JAVA探索 于 2019-06-30 21:18:31 发布

看起来像官文的翻译。

5、

ben发布于博客园

posted @ 2022-09-21 21:59  快乐的欧阳天美1114  阅读(2600)  评论(0编辑  收藏  举报