一、Redis发布订阅原理
Redis的架构包括两个部分:Redis Client和Redis Server,即客户端和服务端。客户端负责向服务器端发送请求并接受来自服务器端的响应。服务器端负责处理客户端请求
发布订阅的框架
其中Publisher(发布)和Subscriber(订阅)为Redis Client,channel为Redis server,而且发布者和订阅者是一对多的关系。
客户端和服务端可以理解为都各自维护着一个channel列表。
- (1)PUBLISH
当客户端向某个频道发送消息时,Redis首先在结构体redisServer中的pubsub_channels中找出键为该频道的结点,遍历该结点的值,找出所有的客户端,将消息发送给这些客户端。然后,遍历结构体Redis Client中的pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。
- 2)SUBSCRIBE
在客户端结构体client中,有一个属性为pubsub_channels,该属性表明了该客户端订阅的所有频道,它是一个字典类型,通过哈希表实现,其中的每个元素都包含了一个键值对以及指向下一个元素的指针,每次订阅都要向其中插入一个结点,键表示订阅的频道,值为空。然后,在表示服务器端的结构体redisServer中,也有一个属性为pubsub_channels,但此处它表示的是该服务器端中的所有频道以及订阅了这个频道的客户端,它也是一个字典类型,插入结点时,键表示频道,值则是订阅了这个频道的所有客户端组成的链表。最后Redis通知客户端其订阅成功。
- (3)PSUBSCRIBE
当客户端订阅某个模式时,Redis同样需要将该模式和该客户端绑定。首先,在结构体client中,有一个属性为pubsub_patterns,该属性表示该客户端订阅的所有模式,它是一个链表类型,每个结点包括了订阅的模式和指向下一个结点的指针,每次订阅某个模式时,都要向其中插入一个结点。然后,在结构体redisServer中,有一个属性也叫pubsub_patterns,它表示了该服务器端中的所有模式和订阅了这些模式的客户端,它也是一个链表类型,插入结点时,每个结点都要包含订阅的模式,以及订阅这个模式的客户端,和指向下一个结点的指针,个人理解为是一种模糊匹配
- (4)UNSUBSCRIBE(退订)
命令可以退订指定的频道, 这个命令执行的是订阅的反操作: 它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。
参考连接:https://redisbook.readthedocs.io/en/latest/feature/pubsub.html
二、REDIS发布订阅和监听REDIS队列的区别
使用jedis的subscribe和publish实现的发布订阅系统 PK 使用jedis的BRPOP和BLPOP实现的阻塞时消息队列
1、redis队列为阻塞队列,获取完一个信息后会主动退出,如果想一直获取信息则需要开启一个监听;而发布订阅中的订阅端是自动完成的监听。
2、redis队列中的数据取出后就消失了,无法满足多端口;而发布订阅可以将数据发布到多个channel。
3、redis队列的数据不取出就会一直在缓存中;而发布订阅中订阅获取的数据不处理就消失了。
三、ActiveMQ和REDIS发布订阅的比较
1、ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等(https://www.cnblogs.com/winner-0715/p/6883212.html),并且支持JMS规范;Redis没有提供对这些协议的支持
2、ActiveMQ提供持久化功能;Redis消息被发送,如果没有订阅者接收,那么消息就会丢失
3、ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端;Redis没有提供消息传输保障
4、ActiveMQ所提供的功能远比Redis发布订阅要复杂,毕竟Redis不是专门做发布订阅的;如果系统能够通过mq实现,则没必要使用mq
四、springboo中RedisTemplate实现发布订阅
maven依赖
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
实现redis发布订阅核心是RedisMessageListenerContainer类。它是Redis订阅发布的监听容器,你的消息发布、订阅配置都必须在这里面实现
- 其中addMessageListener(MessageListenerAdapter,PatternTopic)方法是 新增订阅频道及订阅者,订阅者必须有相关方法处理收到的消息
- MessageListenerAdapter 监听适配器
– MessageListenerAdapter(Object , defaultListenerMethod) 订阅者及其方法 - redisTemplate redis模版类
– convertAndSend(String channel, Object message) 消息发布
具体代码实现
订阅者具体代码实现
核心reidsConfig
/**
* RedisTemplate配置
* @author 王大宝
*/
@Configuration
public class RedisConfiguration {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedisProperties redisProperties;
@Bean
@Primary
/**
*redis连接配置
*/
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration();
standaloneConfig.setHostName(redisProperties.getHost()); //redis地址
standaloneConfig.setPort(redisProperties.getPort());
standaloneConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
//redis库。这里用的都是1号库
standaloneConfig.setDatabase(redisProperties.getDatabase());
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisProperties.getJedis().getPool().getMaxIdle());
poolConfig.setMinIdle(redisProperties.getJedis().getPool().getMinIdle());
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
.connectTimeout(Duration.ofMillis(2000))
.readTimeout(Duration.ofMillis(2000))
.usePooling()
.poolConfig(poolConfig)
.build();
return new JedisConnectionFactory(standaloneConfig, clientConfig);
}
@Bean
public MessageListenerAdapter accessTokenChangeListenerAdapter(AccessTokenChangeEvent accessTokenChangeEvent) {
//监听适配器。这里需要配置具体处理订阅消息的具体方法,即:accessTokenChangeEvent(该类中实现具体处理订阅消息的代码)
MessageListenerAdapter messageListener = new MessageListenerAdapter(accessTokenChangeEvent);
//自定义序列化方式
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
messageListener.setSerializer(jsonRedisSerializer);
return messageListener;
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory redisConnectionFactory,
@Qualifier("accessTokenChangeListenerAdapter") MessageListenerAdapter accessTokenChangeListenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
//订阅频道。和发布消息的频道key保持一致,这里的key值为:access_token_event
ChannelTopic accessTokenTopic = new ChannelTopic(ChannelRedisKey.ACCESS_TOKEN_EVENT);
//向监听容器中新增订阅频道和订阅者。
container.addMessageListener(accessTokenChangeListenerAdapter, accessTokenTopic);
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(2);
taskExecutor.setQueueCapacity(1000);
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("redis-message-listener-container-pool-%d")
.setUncaughtExceptionHandler((thread, exception) ->
logger.error(thread.toString(), exception)
).build();
taskExecutor.setThreadFactory(threadFactory);
taskExecutor.initialize();
container.setTaskExecutor(taskExecutor);
return container;
}
}
其中 accessTokenChangeEvent 为具体的处理订阅消息的实现类,该类中的handleMessage()方法中是你的具体处理逻辑。
例如:
/**
* accessToken订阅事件处理
* @author 王大宝
*/
@Configuration
public class AccessTokenChangeEvent {
private Logger logger = LoggerFactory.getLogger(getClass());
public void handleMessage(Map<String, Object> event, String channel) {
logger.info("收到token变更事件事件:{}", event.toString());
........
....
}
}
这里为什么是handleMessage()方法?因为在创建MessageListenerAdapter 时并没有指定监听方法,默认是handleMessage
发布者代码实现:
发布者相对来说很简单,需要配置一个redisTemplate .。其中注意redis库要与订阅者保持一致。
再数据更改处,或者需要发布的地方使用redisTemplate的convertAndSend方法
例如:
//发布消息。更新内存中token
Map<String, Object> stateEvent = new HashMap<>();
redisTemplate.convertAndSend(ChannelRedisKey.ACCESS_TOKEN_EVENT,stateEvent);