这是一个通常称为发布/订阅(简称Pub/Sub)的模式示例。RedisTemplate类用于消息生成。对于类似于Java EE的消息驱动bean风格的异步接收,Spring Data提供了一个专用的消息监听器容器,用于创建消息驱动的POJO(MDP),对于同步接收,还提供RedisConnection。
org.springframework.data.redis.connection 和 org.springframework.data.redis.listener包提供Redis消息传递的核心功能。
要发布消息,你可以像使用其他操作一样,使用低级的RedisConnection或高级的RedisOperations。这两个实体都提供了publish方法,该方法接受消息和目标通道(channel)作为参数。RedisConnection需要原始数据(字节数组),而RedisOperations允许任意对象作为消息传递,如下面的例子所示:
// send message through connection
RedisConnection con = …
byte[] msg = …
byte[] channel = …
con.pubSubCommands().publish(msg, channel);
// send message through RedisOperations
RedisOperations operations = …
Long numberOfClients = operations.convertAndSend("hello!", "world");
一个相对完整的发布例子:
package com.example.demo;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
public class RedisPublishMessage {
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration =
new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName("localhost");
redisStandaloneConfiguration.setDatabase(0);
redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
redisStandaloneConfiguration.setPort(6379);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
public static void main(String[] args) {
JedisConnectionFactory connectionFactory = new RedisApplication().jedisConnectionFactory();
connectionFactory.afterPropertiesSet();
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setDefaultSerializer(StringRedisSerializer.UTF_8);
template.afterPropertiesSet();
// send message through RedisOperations
RedisOperations operations = template;
Long numberOfClients = operations.convertAndSend("mychannel", "This is Gabriel");
System.out.println(numberOfClients);
}
}
在接收端,可以通过直接命名或使用模式(pattern)匹配来订阅一个或多个通道(channels)。后一种方法非常有用,因为它不仅允许使用一个命令创建多个订阅(subscription),而且还可以监听订阅时尚未创建的通道(只要它们与模式匹配)。
在底层,RedisConnection提供了subscribe和pSubscribe方法,它们分别映射Redis命令的以通道或模式进行订阅。前述2个方法可以使用多个通道或多个模式做为参数。为了更改连接的订阅或检查连接是否在监听,RedisConnection提供了getSubscription和isSubscribed方法。
Spring Data Redis中的Subscription命令是阻塞的。也就是说,在连接上调用subscribe会导致当前线程在开始等待消息时阻塞。只有当订阅被取消时,线程才会被释放,当另一个线程在同一连接上调用unsubscribe或pUnsubscribe时,就会发生这种情况。有关此问题的解决方案,请参阅“2.1 消息监听容器”(本文档稍后部分)。
如前所述,一旦订阅,连接就会开始等待消息。只允许添加新订阅、修改现有订阅和取消现有订阅的命令。调用除subscribe, pSubscribe, unsubscribe, 和 pUnsubscribe之外的任何操作都会引发异常。
为了订阅消息,需要实现MessageListener回调。每次新消息到达时,都会调用回调,并通过onMessage方法运行用户代码。该接口不仅可以访问实际消息,还可以访问接收该消息的通道以及用于匹配订阅的通道的模式(如果有的话)。这些信息使被调用者来区分各种消息,不仅是通过内容,还可以通过检查其他细节。
一个相对完整的订阅例子:
package com.example.demo;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.nio.charset.StandardCharsets;
public class RedisSubscribeMessage {
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration =
new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName("localhost");
redisStandaloneConfiguration.setDatabase(0);
redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
redisStandaloneConfiguration.setPort(6379);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
public static void main(String[] args) {
JedisConnectionFactory connectionFactory = new RedisApplication().jedisConnectionFactory();
connectionFactory.afterPropertiesSet();
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setDefaultSerializer(StringRedisSerializer.UTF_8);
template.afterPropertiesSet();
RedisConnection redisConnection = template.getConnectionFactory().getConnection();
redisConnection.subscribe(new MessageListener() {
@Override
public void onMessage(Message message, byte[] bytes) {
// 收到消息的处理逻辑
System.out.println("Receive message : " + message);
}
}, "mychannel".getBytes(StandardCharsets.UTF_8));
}
}
由于其阻塞特性,低级别订阅(RedisConnection的订阅)没有吸引力,因为它需要对每个监听器进行连接和线程管理。为了缓解这个问题,Spring Data提供了RedisMessageListenerContainer,它完成了所有繁重的工作。如果你熟悉EJB和JMS,你应该会发现这些概念很熟悉,因为它的设计尽可能地接近Spring Framework的支持及其消息驱动的POJO(MDP)。
RedisMessageListenerContainer充当消息监听器容器。它用于从Redis通道(channel)接收消息,并驱动注入其中的MessageListener实例。监听器容器负责消息接收的所有线程,并将消息分派到监听器中进行处理。消息监听器容器是MDP和消息传递提供者之间的中介,负责注册接收消息、资源获取和释放、异常转换等。这使你作为应用程序开发人员能够编写与接收消息相关联的(可能复杂的)业务逻辑,并将Redis基础设施的公式化问题委托给框架。
MessageListener还可以实现SubscriptionListener,以便在确认订阅/取消订阅时接收通知。在同步调用时,监听订阅通知可能很有用。
为了最大限度地减少应用程序占用,RedisMessageListenerContainer允许多个监听器共享一个连接和一个线程,即使它们不共享订阅。因此,无论应用程序跟踪多少监听器或通道,运行时成本在其整个生命周期中都保持不变。另外,容器允许更改运行时配置,以便在应用程序运行时添加或删除监听器,而无需重新启动。此外,容器使用延迟订阅方法,仅在需要时使用RedisConnection。如果所有监听器都被取消订阅,则会自动执行清理,并释放线程。
为了保证消息的异步特性,容器需要一个java.util.concurrent.Executor(或Spring的TaskExecutor)来分发消息。根据负载、监听器的数量和运行时环境,你应该调整executor以更好地满足你的需求。
MessageListenerAdapter类是Spring异步消息传递支持的最后一个组件。简而言之,它允许你将几乎任何类公开为MDP(尽管有一些约束)。以下面的接口定义举例:
public interface MessageDelegate {
void handleMessage(String message);
void handleMessage(Map message);
void handleMessage(byte[] message);
void handleMessage(Serializable message);
// pass the channel/pattern as well
void handleMessage(Serializable message, String channel);
}
请注意,尽管上面的接口没有扩展MessageListener接口,但仍然可以通过使用MessageListenerAdapter类将其用作MDP。还请注意,各种消息处理方法是如何根据它们可以接收和处理的各种消息类型的内容进行强类型化的。此外,消息发送到的通道(channel)或模式(pattern)可以作为第二个String类型的参数传递给方法:
public class DefaultMessageDelegate implements MessageDelegate {
// implementation elided for clarity...
}
注意上面的MessageDelegate接口的实现(上面的DefaultMessageDelegate类)是如何完全没有Redis依赖的。它确实是一个POJO,我们使用以下配置将其创建为MDP:
@Configuration
class MyConfig {
// …
@Bean
DefaultMessageDelegate listener() {
return new DefaultMessageDelegate();
}
@Bean
MessageListenerAdapter messageListenerAdapter(DefaultMessageDelegate listener) {
return new MessageListenerAdapter(listener, "handleMessage");
}
@Bean
RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listener, ChannelTopic.of("chatroom"));
return container;
}
}
监听器主题可以是一个通道(例如,topic=“chatroom”),也可以是一种模式(例如,topic=“*room”)
前面的示例使用Redis命名空间来声明消息监听器容器,并自动将POJO注册为监听器。成熟beans的定义如下:
<bean id="messageListener" class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="redisexample.DefaultMessageDelegate"/>
constructor-arg>
bean>
<bean id="redisContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="messageListeners">
<map>
<entry key-ref="messageListener">
<bean class="org.springframework.data.redis.listener.ChannelTopic">
<constructor-arg value="chatroom"/>
bean>
entry>
map>
property>
bean>
每次接收到消息时,适配器都会自动且透明地在低级(low-level)格式和所需的对象类型之间执行转换(使用配置的RedisSerializer)。由方法调用引起的任何异常都将被捕获并由容器处理(默认情况下,异常将被记录)。
Spring Data提供了ReactiveRedisMessageListenerContainer,它帮助用户完成所有繁重的转换和订阅状态管理工作。
消息监听器容器本身不需要外部线程资源。它使用driver线程来发布消息。
ReactiveRedisConnectionFactory factory = …
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory);
Flux<ChannelMessage<String, String>> stream = container.receive(ChannelTopic.of("my-channel"));
要等待并确保正确的订阅,可以使用receiveLater方法,该方法返回Mono
ReactiveRedisConnectionFactory factory = …
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory);
Mono<Flux<ChannelMessage<String, String>>> stream = container.receiveLater(ChannelTopic.of("my-channel"));
stream.doOnNext(inner -> // notification hook when Redis subscriptions are synchronized with the server)
.flatMapMany(Function.identity())
.…;
如上所述,你可以直接使用ReactiveRedisTemplate订阅通道/模式。这种方法提供了一种直接但有限的解决方案,因为你失去了在初始订阅之后添加订阅的选项。尽管如此,你仍然可以通过返回的Flux来控制消息流,例如使用take(Duration)。当读取完成、出错或取消时,所有绑定的资源将再次释放。
redisTemplate.listenToChannel("channel1", "channel2").doOnNext(msg -> {
// message processing ...
}).subscribe();