SpringBoot整合Redis消息隊列

消息模型

1)隊列模型

隊列模型如圖所示,它具有以下幾個特點,就像我們用微信和好友(羣聊除外)聊天一樣,微信就是這個隊列,我們可以和很多個好友聊天,但是每條消息只能發給一個好友。

  • 只有一個消費者將獲得消息
  • 生產者不需要在接收者消費該消息期間處於運行狀態,接收者也同樣不需要在消息發送時處於運行狀態。
  • 每一個成功處理的消息都由接收者簽收

隊列模型

2)發佈/訂閱模型

發佈/訂閱模型如圖所示,不用說,和訂閱公衆號是一樣的。

  • 多個消費者可以獲得消息
  • 在發佈者和訂閱者之間存在時間依賴性。發佈者需要建立一個topic,以便客戶能夠購訂閱。訂閱者必須保持持續的活動狀態以接收消息,除非訂閱者建立了持久的訂閱。在那種情況下,在訂閱者未連接時發佈的消息將在訂閱者重新連接時重新發布

發佈訂閱模型

Redis實現

  1. 對於隊列模型,我們可以使用redis的list數據結構,通過LPUSH和RPOP來實現一個隊列。
  2. 發佈/訂閱模型就更簡單了,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。

參考:

springboot整合redis消息隊列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章