消息模型
1)隊列模型
隊列模型如圖所示,它具有以下幾個特點,就像我們用微信和好友(羣聊除外)聊天一樣,微信就是這個隊列,我們可以和很多個好友聊天,但是每條消息只能發給一個好友。
- 只有一個消費者將獲得消息
- 生產者不需要在接收者消費該消息期間處於運行狀態,接收者也同樣不需要在消息發送時處於運行狀態。
- 每一個成功處理的消息都由接收者簽收
2)發佈/訂閱模型
發佈/訂閱模型如圖所示,不用說,和訂閱公衆號是一樣的。
- 多個消費者可以獲得消息
- 在發佈者和訂閱者之間存在時間依賴性。發佈者需要建立一個topic,以便客戶能夠購訂閱。訂閱者必須保持持續的活動狀態以接收消息,除非訂閱者建立了持久的訂閱。在那種情況下,在訂閱者未連接時發佈的消息將在訂閱者重新連接時重新發布
Redis實現
- 對於隊列模型,我們可以使用redis的list數據結構,通過LPUSH和RPOP來實現一個隊列。
- 發佈/訂閱模型就更簡單了,redis官方就支持,而且還可以使用PSUBSCRIBE支持模式匹配,使用如下命令,即可訂閱所有f開頭的訂閱,具體可查看文檔。
PSUBSCRIBE f*
3. keyspace notifications(鍵空間通知)
該功能是在redis2.8之後引入的,即客戶端可以通過pub/sub機制,接收key的變更的消息。換句話說,就是redis官方提供了一些topic,幫助我們去監聽redis數據庫中的key,我曾經就使用其中的‘keyevent@0:expired’實現了定時任務。
和SpringBoot整合
首先得介紹一下spring-data-redis中的兩種template的默認Serializer,當然Spring還提供其他的序列化器,具體可查看文檔,也可以自己實現RedisSerializer接口,構建自己的序列化器。
template | default serializer | serialization |
---|---|---|
RedisTemplate | JdkSerializationRedisSerializer | 序列化String類型的key和value |
StringRedisTemplate | StringRedisSerializer | 使用Java序列化 |
消息隊列模型
消息隊列,這個就需要自己造輪子了,在Spring中使用redisTemlate操作數據庫,而對於不同的數據類型則需要不同的操作方式,如下表格所示,具體還是請看官方文檔。
實現隊列選擇list數據結構,redisTemplate.opsForList()使用起來非常簡單,和redis命令基本一致。
數據類型 | 操作方式 |
---|---|
string | redisTemplate.opsForValue() |
hash | redisTemplate.opsForHash() |
list | redisTemplate.opsForList() |
set | redisTemplate.opsForSet() |
1.先定義一個消息的POJO(MessageEntity實體類)
2.配置spring data 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.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.johnfnash.learn.redis.queue.entity.MessageEntity;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, MessageEntity> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, MessageEntity> template = new RedisTemplate<String, MessageEntity>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
用到的 redis 配置信息如下:
#數據庫索引(默認爲0)
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
#連接超時時間
spring.redis.timeout=10000
#最大連接數
spring.redis.jedis.pool.max-active=8
#最大阻塞等待時間(負數表示沒限制)
spring.redis.jedis.pool.max-wait=-1
#最大空閒
spring.redis.jedis.pool.max-idle=8
#最小空閒
spring.redis.jedis.pool.min-idle=0
# redis消息隊列鍵名
redis.queue.key=queue
# redis消息隊列讀取消息超時時間,單位:秒
redis.queue.pop.timeout=1000
3.消息的消費者,消費者需要不斷輪詢隊列,有消息便取出來。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 消息的消費者,消費者需要不斷的去輪詢隊列,有消息就取出來
*/
@Service
public class MessageConsumerService extends Thread {
@Resource
private RedisTemplate<String, MessageEntity> redisTemplate;
private volatile boolean flag = true;
@Value("${redis.queue.key}")
private String queueKey;
@Value("${redis.queue.pop.timeout}")
private Long popTimeout;
@Override
public void run() {
try {
MessageEntity message;
while (flag && !Thread.currentThread().isInterrupted()) {
message = redisTemplate.opsForList().rightPop(queueKey, popTimeout, TimeUnit.SECONDS);
System.out.println("接收到了" + message);
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
4.消息的生產者,這個類提供一個發送消息的方法。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* 生產者:提供一個發送消息的方法
*/
@Service
public class MessageProducerService {
@Autowired
private RedisTemplate<String, MessageEntity> redisTemplate;
@Value("${redis.queue.key}")
private String queueKey;
public Long sendMeassage(MessageEntity message) {
System.out.println("發送了" + message);
return redisTemplate.opsForList().leftPush(queueKey, message);
}
}
測試
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class QueueTest {
@Autowired
private MessageProducerService producer;
@Autowired
private MessageConsumerService consumer;
/**
* 消息隊列模型測試
*/
@Test
public void testQueue() {
consumer.start();
producer.sendMeassage(new MessageEntity("1", "aaaa"));
producer.sendMeassage(new MessageEntity("2", "bbbb"));
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
consumer.interrupt();
}
}
輸出信息如下:
2020-06-28 11:27:08.136 INFO 81195 --- [ main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-06-28 11:27:08.147 INFO 81195 --- [ main] c.test.springboot2_test.queue.QueueTest : Started QueueTest in 11.012 seconds (JVM running for 12.666)
發送了MessageEntity [id=1, content=aaaa]
發送了MessageEntity [id=2, content=bbbb]
接收到了MessageEntity [id=1, content=aaaa]
接收到了MessageEntity [id=2, content=bbbb]
Redis command interrupted; nested exception is io.lettuce.core.RedisCommandInterruptedException: Command interrupted
至此,消息隊列的方式也整合完成了。
雖然redisTemplate是線程安全的,但是如果一個隊列有多個接收者的話,可能也還需要考慮一下併發的問題。
發佈/訂閱模型
發佈/訂閱模型,因爲Spring官方給了示例。但是呢,示例裏面的消息是String類型,對於我們的業務來說,可能更需要一個POJO(MessageEntity實體類)
1.先學習下org.springframework.data.redis.listener.adapter.MessageListenerAdapter源碼如下,可以看到,如果使用StringRedisTemplate的話,默認都是使用StringRedisSerializer來反序列化,而如果想主動接收消息,則需要實現MessageListener接口。
/**
* Standard Redis {@link MessageListener} entry point.
* <p>
* Delegates the message to the target listener method, with appropriate conversion of the message argument. In case
* of an exception, the {@link #handleListenerException(Throwable)} method will be invoked.
*
* @param message the incoming Redis message
* @see #handleListenerException
*/
public void onMessage(Message message, byte[] pattern) {
try {
// Check whether the delegate is a MessageListener impl itself.
// In that case, the adapter will simply act as a pass-through.
if (delegate != this) {
if (delegate instanceof MessageListener) {
((MessageListener) delegate).onMessage(message, pattern);
return;
}
}
// Regular case: find a handler method reflectively.
Object convertedMessage = extractMessage(message);
String convertedChannel = stringSerializer.deserialize(pattern);
// Invoke the handler method with appropriate arguments.
Object[] listenerArguments = new Object[] { convertedMessage, convertedChannel };
invokeListenerMethod(invoker.getMethodName(), listenerArguments);
} catch (Throwable th) {
handleListenerException(th);
}
}
/**
* Extract the message body from the given Redis message.
*
* @param message the Redis <code>Message</code>
* @return the content of the message, to be passed into the listener method as argument
*/
protected Object extractMessage(Message message) {
if (serializer != null) {
return serializer.deserialize(message.getBody());
}
return message.getBody();
}
/**
* Initialize the default implementations for the adapter's strategies.
*
* @see #setSerializer(RedisSerializer)
* @see JdkSerializationRedisSerializer
*/
protected void initDefaultStrategies() {
RedisSerializer<String> serializer = new StringRedisSerializer();
setSerializer(serializer);
setStringSerializer(serializer);
}
2.spring data redis實現發佈與訂閱需要配置以下信息:
- Topic
- MessageListener
- RedisMessageListenerContainer
1)用到的相關依賴:
<!-- 集成redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)配置 spring data redis:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.springboot2_test.queue.ConsumerRedisListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
/**
* Redis 緩存配置類
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Autowired
private LettuceConnectionFactory connectionFactory;
@Bean
public ConsumerRedisListener consumeRedis() {
return new ConsumerRedisListener();
}
@Bean
public ChannelTopic topic() {
return new ChannelTopic("topic");
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(consumeRedis(), topic());
return container;
}
}
3)實現一個Object類型的 topic MessageListener
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
public class ConsumerRedisListener implements MessageListener {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
doBusiness(message);
}
/**
* 打印 message body 內容
* @param message
*/
public void doBusiness(Message message) {
Object value = redisTemplate.getValueSerializer().deserialize(message.getBody());
System.out.println("consumer message: " + value.toString());
}
}
4)其他(最簡單的application.properties配置如下)
#數據庫索引(默認爲0)
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
#連接超時時間
spring.redis.timeout=10000
#最大連接數
spring.redis.jedis.pool.max-active=8
#最大阻塞等待時間(負數表示沒限制)
spring.redis.jedis.pool.max-wait=-1
#最大空閒
spring.redis.jedis.pool.max-idle=8
#最小空閒
spring.redis.jedis.pool.min-idle=0
通過上面四步,簡單的訂閱者就做好了,通過以下代碼可以發佈一個消息,同時可以查看到控制檯會有訂閱者消費信息打印出來:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.Date;
@RunWith(SpringRunner.class)
@SpringBootTest
public class QueueTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 發佈訂閱模型測試
*/
@Test
public void testSubscribe() {
String channel = "topic";
System.out.println("開始發送消息。。。。。。。。。。。。。。");
redisTemplate.convertAndSend(channel, "hello world");
redisTemplate.convertAndSend(channel, new Date(System.currentTimeMillis()));
redisTemplate.convertAndSend(channel, new MessageEntity("1", "object"));
System.out.println("發送消息結束。。。。。。。。。。。。。。");
}
}
這裏用到了一個實體類用於測試。
import java.io.Serializable;
public class MessageEntity implements Serializable {
private static final long serialVersionUID = 8632296967087444509L;
private String id;
private String content;
public MessageEntity() {
super();
}
public MessageEntity(String id, String content) {
super();
this.id = id;
this.content = content;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "MessageEntity [id=" + id + ", content=" + content + "]";
}
}
輸出結果如下:
consumer message: hello world
consumer message: Sat Feb 23 13:04:40 CST 2019
consumer message: MessageEntity [id=1, content=object]
用 spring data redis 來實現 redis 訂閱者,本質上還是Listener模式,只需要配置Topic, MessageListener 和 RedisMessageListenerContainer就可以了。同時,發佈時,只需要使用 redisTemplate 的 convertAndSend方法即可topic來發布message。
參考: