一、背景
不同的mq中间件,不仅支持发送队列消息,也都可以发送广播消息。
有时候,我们的业务逻辑需要发送广播消息,却不想引入mq中间件时,推荐你使用redis的topic机制来实现。
原因是:它比较轻量级,大多数的项目都会依赖redis数据库,对部署的依赖开销变小。
本文就如何实现redis发布和订阅广播消息作一个示例,希望可以帮助到你。
二、典型的生产者/消费者
所以,我们有以下几个角色:
- 消息体/事件
- 生产者
- 消费者
三、代码实现
1、配置监听
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* @author xxx
*/
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setHashValueSerializer(RedisSerializer.string());
template.afterPropertiesSet();
return template;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.json());
template.setHashKeySerializer(RedisSerializer.string());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 可以添加多个 messageListener,配置不同的交换机
// REDIS_TOPIC_REMOVE_DELAY_TASK是一个常量,定义你的队列名称
container.addMessageListener(listenerAdapter, new PatternTopic(REDIS_TOPIC_REMOVE_DELAY_TASK));
return container;
}
// RemoveDelayTaskEventListener是消息监听者类
@Bean
MessageListenerAdapter listenerAdapter(RemoveDelayTaskEventListener removeDelayTaskEventListener) {
MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(removeDelayTaskEventListener, "onMessage");
return listenerAdapter;
}
}
这里我们定义了两个RedisTemplate对象,在发送消息的时候有所区别。
- RedisTemplate<String, String> stringRedisTemplate 要求redis value是一个string对象
- RedisTemplate<String, Object> redisTemplate 允许redis value是一个Object对象,会对Object对象自动序列化,不用程序关注。
2、消息生产者
这里介绍两种发送方式:
- 使用stringRedisTemplate
@Autowired
private RedisTemplate<String, String> stringRedisTemplate;
// 需要对对象进行Json序列化JSON.toJSONString()
stringRedisTemplate.convertAndSend(REDIS_TOPIC_REMOVE_DELAY_TASK,
JSON.toJSONString(RemoveDelayTaskEvent.builder()
.transNo(transNo)
.ipAddress(ipAddress)
.build()));
- 使用redisTemplate
@Autowired
private RedisTemplate<String, Object> redisTemplate;
redisTemplate.convertAndSend(REDIS_TOPIC_REMOVE_DELAY_TASK, RemoveDelayTaskEvent.builder()
.transNo(transNo)
.ipAddress(ipAddress)
.build());
3、消息监听/消费者
无论你上面使用哪种方式发送消息,消息监听者都是需要反序列化。
import cn.hutool.core.net.NetUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Service;
/**
* redis消息监听者.
*
* @author xxx
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RemoveDelayTaskEventListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
final String topic = new String(message.getChannel());
final String body = new String(message.getBody());
if (log.isInfoEnabled()) {
log.info("订阅redis广播消息, topic={}, body={}", topic, body);
}
if (StringUtils.isBlank(body)) {
return;
}
final RemoveDelayTaskEvent event = JSONObject.parseObject(body, RemoveDelayTaskEvent.class);
final String transNo = event.getTransNo();
final String ipAddress = event.getIpAddress();
// 逻辑处理(略)
}
}
输出的日志示例:
订阅redis广播消息, topic=removeDelayTaskTopic, body={"@class":"com.xxx.application.message.RemoveDelayTaskEvent","transNo":"12345633089","ipAddress":""}
4、消息体RemoveDelayTaskEvent
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class RemoveDelayTaskEvent {
/**
* 交易流水号
*/
private String transNo;
/**
* ip地址
*/
private String ipAddress;
}
四、模拟多个监听者
listenerAdapter1和listenerAdapter2是两个消息监听者,它们都监听同一个topi。(两个消息监听者都将消费同一个消息,也就是广播模式下,一个消息为多个监听者消费)
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 添加消息监听者listenerAdapter1,配置同一个交换机REDIS_TOPIC_REMOVE_DELAY_TASK
container.addMessageListener(listenerAdapter1, new PatternTopic(REDIS_TOPIC_REMOVE_DELAY_TASK));
// 添加消息监听者listenerAdapter2,配置同一个交换机REDIS_TOPIC_REMOVE_DELAY_TASK
container.addMessageListener(listenerAdapter2, new PatternTopic(REDIS_TOPIC_REMOVE_DELAY_TASK));
return container;
}
@Bean
MessageListenerAdapter listenerAdapter1(RemoveDelayTaskEventListener removeDelayTaskEventListener) {
MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(removeDelayTaskEventListener, "onMessage");
return listenerAdapter;
}
@Bean
MessageListenerAdapter listenerAdapter2(RemoveDelayTaskEventListener removeDelayTaskEventListener) {
MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(removeDelayTaskEventListener, "onMessage");
return listenerAdapter;
}
五、总结
redis本身是绝大数应用已依赖的中间件,所以如果遇到发布广播消息的场景,可以考虑使用redis topic机制,轻松实现。