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服务器:终端#1;ben发布于博客园
使用 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发布于博客园