关于 Redis 发布订阅的功能介绍可以参考:订阅与发布。下面我来介绍下 Redis 发布订阅功能的特性和适用场景。
由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。例如:
总之,Redis 发布订阅功能足够简单,如果没有过多的要求,且不想搭建 Kafka、RabbitMQ 这样的可靠型消息系统时,可以考虑尝试使用 Redis。
Spring Data Redis 实现发布订阅功能非常简单,只有这样的几个类:Topic、MessageListener、RedisMessageListenerContainer
。下面对它们进行解释:
public interface MessageListener {
/**
* Callback for processing received objects through Redis.
*
* @param message message
* @param pattern pattern matching the channel (if specified) - can be null
*/
void onMessage(Message message, byte[] pattern);
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.1.3.RELEASEversion>
dependency>
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private String userName;
private String password;
private int age;
private String email;
private Date createDate;
}
@Configuration
public class RedisConfig {
// 默认情况下RedisTemplate模板只能支持字符串,我们自定义一个RedisTemplate,设置序列化器,这样我们可以很方便的操作实例对象。
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
// 配置用户注册消息监听器
@Bean
public UserRegisterMessageListener userRegisterMessageListener() {
return new UserRegisterMessageListener();
}
// 配置用户注销消息监听器
@Bean
public UserLogoutMessageListener userLogoutMessageListener() {
return new UserLogoutMessageListener();
}
// 配置用户消息(包括注册、注销等)监听器
@Bean
public UserMessageListener userMessageListener() {
return new UserMessageListener();
}
// 将消息监听器绑定到消息容器
@Bean
public RedisMessageListenerContainer messageListenerContainer(LettuceConnectionFactory lettuceConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(lettuceConnectionFactory);
UserRegisterMessageListener userRegisterMessageListener = userRegisterMessageListener();
// 订阅用户注册消息话题
container.addMessageListener(userRegisterMessageListener, userRegisterMessageListener.topic());
UserLogoutMessageListener userLogoutMessageListener = userLogoutMessageListener();
// 订阅用户注销消息话题
container.addMessageListener(userLogoutMessageListener, userLogoutMessageListener.topic());
UserMessageListener userMessageListener = userMessageListener();
// 订阅用户消息(包括注册、注销等)话题
container.addMessageListener(userMessageListener, userMessageListener.topic());
return container;
}
}
public interface MessageEventListener extends MessageListener {
/**
* 订阅者订阅的话题
*
* @return topic
*/
Topic topic();
}
@Slf4j
public class UserRegisterMessageListener implements MessageEventListener {
public static final String TOPIC_NAME = "jaemon:user:register";
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
String channel = new String(message.getChannel());
// 如果是 ChannelTopic, 则 channel 和 new String(pattern) 的值是相同的
log.info("用户注册事件: body=[{}], channel=[{}], pattern=[{}]", body, channel, new String(pattern));
}
@Override
public Topic topic() {
return new ChannelTopic(TOPIC_NAME);
}
}
@Slf4j
public class UserLogoutMessageListener implements MessageEventListener {
public static final String TOPIC_NAME = "jaemon:user:logout";
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
String channel = new String(message.getChannel());
// 如果是 ChannelTopic, 则 channel 和 new String(pattern) 的值是相同的
log.info("用户注销事件: body=[{}], channel=[{}], pattern=[{}]", body, channel, new String(pattern));
}
@Override
public Topic topic() {
return new ChannelTopic(TOPIC_NAME);
}
}
@Slf4j
public class UserMessageListener implements MessageEventListener {
public static final String TOPIC_NAME = "jaemon:user:*";
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
String channel = new String(message.getChannel());
// 如果是 ChannelTopic, 则 channel 和 new String(pattern) 的值是相同的
log.info("用户事件: body=[{}], channel=[{}], pattern=[{}]", body, channel, new String(pattern));
}
@Override
public Topic topic() {
return new PatternTopic(TOPIC_NAME);
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {AssetApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RedisPubSubTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void doJob() {
// 发布用户注册消息
redisTemplate.convertAndSend(
UserRegisterMessageListener.TOPIC_NAME,
new User("jaemon", "123456", 20, "[email protected]", new Date())
);
// 发布用户注销消息
redisTemplate.convertAndSend(
UserLogoutMessageListener.TOPIC_NAME,
new User("jaemon", "7654321", 20, "[email protected]", new Date())
);
}
}
用户注册事件: body=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"123456","age":20,"email":"[email protected]","createDate":["java.util.Date",1596709760919]}], channel=[jaemon:user:register], pattern=[jaemon:user:register]
用户事件: body=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"123456","age":20,"email":"[email protected]","createDate":["java.util.Date",1596709760919]}], channel=[jaemon:user:register], pattern=[jaemon:user:*]
用户注销事件: body=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"7654321","age":20,"email":"[email protected]","createDate":["java.util.Date",1596709761023]}], channel=[jaemon:user:logout], pattern=[jaemon:user:logout]
用户事件: body=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"7654321","age":20,"email":"[email protected]","createDate":["java.util.Date",1596709761023]}], channel=[jaemon:user:logout], pattern=[jaemon:user:*]
@Configuration
public class RedisConfig {
// 默认情况下RedisTemplate模板只能支持字符串,我们自定义一个RedisTemplate,设置序列化器,这样我们可以很方便的操作实例对象。
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
// 配置用户注册消息监听器
@Bean(name = "userRegisterMessageListenerAdapter")
public MessageListenerAdapter userRegisterMessageListenerAdapter(UserRegisterMessageListener userRegisterMessageListener) {
return new MessageListenerAdapter(userRegisterMessageListener);
}
// 配置用户注销消息监听器
@Bean(name = "userLogoutMessageListenerAdapter")
public MessageListenerAdapter userLogoutMessageListenerAdapter(UserLogoutMessageListener userLogoutMessageListener) {
return new MessageListenerAdapter(userLogoutMessageListener);
}
// 配置用户消息(包括注册、注销等)监听器
@Bean(name = "userMessageListenerAdapter")
public MessageListenerAdapter userMessageListenerAdapter(UserMessageListener userMessageListener) {
// 可指定订阅者接受消息的方法
return new MessageListenerAdapter(userMessageListener, "receiveMessage");
}
// 将消息监听器绑定到消息容器
@Bean
public RedisMessageListenerContainer messageListenerContainer(
LettuceConnectionFactory lettuceConnectionFactory,
MessageListenerAdapter userRegisterMessageListenerAdapter,
MessageListenerAdapter userLogoutMessageListenerAdapter,
MessageListenerAdapter userMessageListenerAdapter
) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(lettuceConnectionFactory);
// 订阅用户注册消息话题
container.addMessageListener(userRegisterMessageListenerAdapter, new ChannelTopic(UserRegisterMessageListener.TOPIC_NAME));
// 订阅用户注销消息话题
container.addMessageListener(userLogoutMessageListenerAdapter, new ChannelTopic(UserLogoutMessageListener.TOPIC_NAME));
// 订阅用户消息(包括注册、注销等)话题
container.addMessageListener(userMessageListenerAdapter, new PatternTopic(UserMessageListener.TOPIC_NAME));
return container;
}
}
@Slf4j
@Component
public class UserRegisterMessageListener {
public static final String TOPIC_NAME = "jaemon:user:register";
public void handleMessage(String message) {
log.info("用户注册事件: message=[{}]", message);
}
}
@Slf4j
@Component
public class UserLogoutMessageListener {
public static final String TOPIC_NAME = "jaemon:user:logout";
public void handleMessage(String message) {
log.info("用户注销事件: message=[{}]", message);
}
}
@Slf4j
@Component
public class UserMessageListener {
public static final String TOPIC_NAME = "jaemon:user:*";
public void receiveMessage(String message) {
log.info("用户事件: message=[{}]", message);
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {AssetApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RedisPubSubTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void doJob() {
// 发布用户注册消息
redisTemplate.convertAndSend(
UserRegisterMessageListener.TOPIC_NAME,
new User("jaemon", "123456", 20, "[email protected]", new Date())
);
// 发布用户注销消息
redisTemplate.convertAndSend(
UserLogoutMessageListener.TOPIC_NAME,
new User("jaemon", "7654321", 20, "[email protected]", new Date())
);
}
}
用户注册事件: message=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"123456","age":20,"email":"[email protected]","createDate":["java.util.Date",1596765582710]}]
用户事件: message=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"123456","age":20,"email":"[email protected]","createDate":["java.util.Date",1596765582710]}]
用户注销事件: message=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"7654321","age":20,"email":"[email protected]","createDate":["java.util.Date",1596765582773]}]
用户事件: message=[{"@class":"com.jaemon.entity.User","userName":"jaemon","password":"7654321","age":20,"email":"[email protected]","createDate":["java.util.Date",1596765582773]}]
比如渠道在调支付平台的时候,我们可以用回调的方式给支付平台一个我们的回调接口来通知我们支付状态,还可以利用Redis的发布订阅来实现。比如我们发起支付的同时订阅频道pay_notice_
+ wk
(假如我们的渠道标识是wk,不能让其他渠道也订阅这个频道),当支付平台处理完成后,支付平台往该频道发布消息,告诉频道的订阅者该订单的支付信息及状态。收到消息后,根据消息内容更新订单信息及后续操作。
当很多人都调用支付平台时,支付时都去订阅同一个频道会有问题。比如用户A支付完订阅频道pay_notice_wk
,在支付平台未处理完时,用户B支付完也订阅了pay_notice_wk
,当A收到通知后,接着B的支付通知也发布了,这时渠道收不到第二次消息发布。因为同一个频道收到消息后,订阅自动取消,也就是订阅是一次性的。
所以我们订阅的订单支付状态的频道就得唯一,一个订单一个频道,我们可以在频道上加上订单号pay_notice_wk
+orderNo保证频道唯一。这样我们可以把频道号在支付时当做参数一并传过去,支付平台处理完就可以用此频道发布消息给我们了。(实际大多接口用回调通知,因为用Redis发布订阅限制条件苛刻,系统间必须共用一套Redis)
比如通过跑批系统通知应用系统做一些事(跑批系统无法拿到用户数据,且应用系统又不能做定时任务的情况下)。
如每天凌晨3点提前加载一些用户的用户数据到Redis,应用系统不能做定时任务,可以通过系统公共的Redis来由跑批系统发布任务给应用系统,应用系统收到指令,去做相应的操作。
这里需要注意的是在线上集群部署的情况下,所有服务实例都会收到通知,都要做同样的操作吗?完全没必要。可以用Redis实现锁机制,其中一台实例拿到锁后执行任务。另外如果任务比较耗时,可以不用锁,可以考虑一下任务分片执行。当然这不在本文的讨论范畴,这里不在赘述。
众所周知,我们用Redis无非就是将系统中不怎么变的、查询又比较频繁的数据缓存起来,例如我们系统首页的轮播图啊,页面的动态链接啊,一些系统参数啊,公共数据啊都加载到Redis,然后有个后台管理系统去配置修改这些数据。
打个比方我们首页的轮播图要再增加一个图,那我们就在后管系统加上,加上就完事了吗?当然没有,因为Redis里还是老数据。那你会说不是有过期时间吗?是的,但有的过期时间设置的较长如24小时并且我们想立即生效怎么办?这时候我们就可以利用Redis的发布订阅机制来实现数据的实时刷新。当我们修改完数据后,点击刷新按钮,通过发布订阅机制,订阅者接收到消息后调用重新加载的方法即可。