前言
Redis可以通过发布订阅模式、轮询机制实现消息队列。
由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。
一、Redis发布订阅
1.Redis发布订阅模式实现原理
服务器中维护着一个pubsub_channels字典,所有的频道和订阅关系都存储在这里。字典的键为频道的名称,而值为订阅频道的客户端链表。
1. 当有新的客户端订阅某个频道时,会发生两种情况中的一种:
(1)如果频道已经存在,则新的客户端会添加到pubsub_channels对应频道的链表末尾
(2)如果频道原本不存在,则会为频道创建一个键,该客户端成为链表的第一个元素
2. 当一个客户端退订一个频道的时候:
pubsub_channels对应键的链表会删除该客户端
3. 发送信息
服务器会遍历pubsub_channels中对应键的链表,向每一个客户端发送信息
服务器还维护着一个pubsub_patterns链表,链表的pattern属性记录了被订阅的模式,而client属性记录了订阅模式的客户端
1. 当有新的客户端订阅某个模式的时,会进行如下步骤:
(1)创建一个链表节点,pattern属性记录订阅的模式,client记录订阅模式的客户端
(2)将这个链表节点添加到pubsub_patterns链表中
2. 当一个客户端退订某一个模式的时候:
服务器遍历pubsob_patterns找到对应的pattern同时也是对应该client客户端的节点,将改节点删除
3. 发送信息
服务器遍历pubsub_channels,查找与channels频道相匹配的模式麻将消息发送给订阅了这些模式的客户端。
2.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.redis配置
public class RedisConfig {
//Key的过期时间
private Duration timeToLive = Duration.ofDays(1);
/**
* redis模板,存储关键字是字符串,值jackson2JsonRedisSerializer是序列化后的值
*
* @param
* @return org.springframework.data.redis.core.RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//使用StringRedisSerializer来序列化和反序列化redis的key值
RedisSerializer redisSerializer = new StringRedisSerializer();
//key
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
//value
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.
defaultCacheConfig().
entryTtl(this.timeToLive). //Key过期时间 1天
serializeKeysWith(RedisSerializationContext.SerializationPair.
fromSerializer(new StringRedisSerializer())).
serializeValuesWith(RedisSerializationContext.SerializationPair.
fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
4.redis监听
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.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import java.util.concurrent.CountDownLatch;
@Configuration
public class RedisMessageListener {
/**
* 创建连接工厂
*
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter,
MessageListenerAdapter listenerAdapterWang,
MessageListenerAdapter listenerAdapterTest2) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//(不同的监听器可以收到同一个频道的信息)接受消息的频道
container.addMessageListener(listenerAdapter, new PatternTopic("phone"));
container.addMessageListener(listenerAdapterWang, new PatternTopic("phone"));
container.addMessageListener(listenerAdapterTest2, new PatternTopic("phoneTest2"));
return container;
}
/**
* 绑定消息监听者和接收监听的方法
*
* @param receiver
* @return
*/
@Bean
public MessageListenerAdapter listenerAdapter(ReceiverRedisMessage receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
@Bean
public MessageListenerAdapter listenerAdapterWang(ReceiverRedisMessage receiver) {
return new MessageListenerAdapter(receiver, "receiveMessageWang");
}
/**
* 绑定消息监听者和接收监听的方法
*
* @param receiver
* @return
*/
@Bean
public MessageListenerAdapter listenerAdapterTest2(ReceiverRedisMessage receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage2");
}
/**
* 注册订阅者
*
* @param latch
* @return
*/
@Bean
ReceiverRedisMessage receiver(CountDownLatch latch) {
return new ReceiverRedisMessage(latch);
}
/**
* 计数器,用来控制线程
*
* @return
*/
@Bean
public CountDownLatch latch() {
return new CountDownLatch(1);//指定了计数的次数 1
}
}
5.redis消息接收类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.CountDownLatch;
public class ReceiverRedisMessage {
private CountDownLatch latch;
@Autowired
public ReceiverRedisMessage(CountDownLatch latch) {
this.latch = latch;
}
/**
* 队列消息接收方法
*
* @param jsonMsg
*/
public void receiveMessage(String jsonMsg) {
log.info("[开始消费REDIS消息队列phone数据...]");
try {
System.out.println(jsonMsg);
log.info("[消费REDIS消息队列phone数据成功.]");
} catch (Exception e) {
log.error("[消费REDIS消息队列phone数据失败,失败信息:{}]", e.getMessage());
}
latch.countDown();
}
public void receiveMessageWang(String jsonMsg) {
log.info("[开始消费REDIS消息队列phone数据...]");
try {
System.out.println(jsonMsg);
log.info("[消费REDIS消息队列phone数据成功.]");
} catch (Exception e) {
log.error("[消费REDIS消息队列phone数据失败,失败信息:{}]", e.getMessage());
}
latch.countDown();
}
/**
* 队列消息接收方法
*
* @param jsonMsg
*/
public void receiveMessage2(String jsonMsg) {
log.info("[开始消费REDIS消息队列phoneTest2数据...]");
try {
System.out.println(jsonMsg);
/**
* 此处执行自己代码逻辑 操作数据库等
*/
log.info("[消费REDIS消息队列phoneTest2数据成功.]");
} catch (Exception e) {
log.error("[消费REDIS消息队列phoneTest2数据失败,失败信息:{}]", e.getMessage());
}
latch.countDown();
}
}
6.代码测试
public void redisTest() {
//这个是测试同一个频道,不同的订阅者收到相同的信息,“phone”也就是topic也可以理解为频道
redisTemplate.convertAndSend("phone", "223333");
//这个phoneTest2是另外的一个频道
// redisTemplate.convertAndSend("phoneTest2", "34555665");
}
二、Redis轮询机制
1.原理
Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以实现一个高性能的优先队列, 在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是4294967295。从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。
2.生产者
public R saveUserTicket(String phoneNum) {
redisTemplate.opsForList().leftPush("ticket:Data", phoneNum);
return R.ok();
}
3.消费者
@Scheduled(fixedRate = 1)
public synchronized void consumer() {
String message = redisTemplate.opsForList().rightPop("ticket:Data", 5, TimeUnit.SECONDS);
if (!StringUtils.isEmpty(message)){
//数据库操作
}
}
4.优化
如上述代码,如果此时队列为空,消费者依然会频繁拉取数据,造成CPU空转,不仅占用CPU资源还对Redis造成压力。因此当队列为空时我们可以休眠一段时间,再进行拉取。
实现如下
@Scheduled(fixedRate = 1)
public synchronized void consumer() throws InterruptedException {
long a = redisTemplate.opsForList().size("ticket:Data");
if (a == 0) {
TimeUnit.SECONDS.sleep(1);//等待时间
}
String message = redisTemplate.opsForList().rightPop("ticket:Data", 5, TimeUnit.SECONDS);
if (!StringUtils.isEmpty(message)){
//数据库操作
}
}