使用Redis实现消息队列
消息队列
什么是消息队列?
- 消息队列就是一个保存消息的容器,它具有先进先出的特性
为什么会出现消息队列?
- 异步:场景的B/S架构下,客户端向服务器发送请求,但是服务器处理这个消息需要花费很长的时间,如果客户端一直等到服务器处理完消息,会造成客户端的系统资源浪费;而使用消息队列后,服务器直接将消息推送到消息队列中,由专门的处理消息程序处理消息,这样客户端就不必花费大量时间等待服务器的响应了。
- 解耦:传统的软件开发模式,模块之间的调用时直接调用,这样的系统很不利于系统的扩展,同时,模块之间的相互调用,数据之间的共享问题也很大,每个模块都要时时刻刻考虑其他模块会不会挂历;使用消息队列之后,模块之间不直接调用,而是通过数据,而且当某个模块挂了以后,数据仍旧会保存在消息队列中。最典型的就是生产者-消费者模式。
- 削峰填谷:某一时刻,系统的并发请求暴增,原因超过了系统的最大处理能力后,如果不做任何处理,系统会崩溃;使用消息队列以后,服务器把请求推送到消息队列中,由专门的处理消息程序以合理的速度消费消息,降低服务器的压力。
springboot集成redis
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
- 使用redis作为消息队列,看到以前有人将消息插入到redis中,再进行读取,但这样就没有发送消息这一步骤了。我们这里使用了RedisTemplate.convertAndSend()方法和一个MessageListener实现类。
RedisTemplate配置
import com.jack.redismq.consumer.RedisListener;
import com.jack.redismq.util.RedisObjectSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Created by zhang_j on 2019/12/3
*/
@Configuration
public class RedisConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisConfig.class);
/**
* RedisTemplate解决乱码问题
*/
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
LOGGER.info("redis序列化配置开始");
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
//string序列化方式
// RedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
RedisObjectSerializer redisObjectSerializer = new RedisObjectSerializer();
//设置默认序列化方式
template.setDefaultSerializer(redisObjectSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(redisObjectSerializer);
LOGGER.info("redis序列化配置结束");
return template;
}
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(new RedisListener(),new PatternTopic("demo-channel"));
return container;
}
}
- 我们这边使用自己的序列化和反序列化方式,因为感觉使用GenericJackson2JsonRedisSerializer不是特别友好。
- 代码如下:
public class RedisObjectSerializer implements RedisSerializer<Object> {
static final byte[] EMPTY_ARRAY = new byte[0];
@Override
public Object deserialize(byte[] bytes) {
if (isEmpty(bytes)) {
return null;
}
ObjectInputStream oii = null;
ByteArrayInputStream bis = null;
bis = new ByteArrayInputStream(bytes);
try {
oii = new ObjectInputStream(bis);
Object obj = oii.readObject();
return obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return EMPTY_ARRAY;
}
ObjectOutputStream obi = null;
ByteArrayOutputStream bai = null;
try {
bai = new ByteArrayOutputStream();
obi = new ObjectOutputStream(bai);
obi.writeObject(object);
byte[] byt = bai.toByteArray();
return byt;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private boolean isEmpty(byte[] data) {
return (data == null || data.length == 0);
}
}
消费者
-
我们实现的MessageListener接口便是监听到队列中消息的回调方法。
-
message:redis消息类,该类中仅有两个方法
- byte[] getBody():以二进制形式获取消息体
- byte[] getChannel(): 以二进制形式获取消息通道
-
pattern:二进制形式的消息通道,和message.getChannel()返回值相同
-
-
简单Redis队列监听器代码如下:
public class RedisListener implements MessageListener {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisListener.class);
@Override
public void onMessage(Message message, byte[] pattern) {
LOGGER.info("从消息通道={}监听到消息",new String(pattern));
LOGGER.info("从消息通道={}监听到消息",new String(message.getChannel()));
LOGGER.info("元消息={}",new String(message.getBody()));
/**
* 新建一个用于反序列化的对象,注意这里的对象和前面配置的一样
* 因为我前面设置的默认序列化方式为GenericJackson2JsonRedisSerializer
* 所以这里的实现方式为GenericJackson2JsonRedisSerializer
*/
// RedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
RedisObjectSerializer redisObjectSerializer = new RedisObjectSerializer();
LOGGER.info("反序列化后的消息={}",redisObjectSerializer.deserialize(message.getBody()));
}
}
- 注意这里的序列化方式需要与配置的一致
- 在实现完队列监听器之后,需要将这个监听添加到redis队列监听器容器中,注意这里需要放到配置中,代码就是RedisConfig中的container方法。
- 注意container方法中的管道名要和推送消息的管道名一致
生产者
@Service
public class Producer {
@Autowired
private RedisTemplate redisTemplate;
public void produce(Object msg){
redisTemplate.convertAndSend("demo-channel",msg);
}
}
测试
- 这里我们新建了一个User类用于测试
public class User implements Serializable {
private static final long serialVersionUID = -8289770787953160443L;
public User(Integer userId, String userName, String password, String phone) {
this.userId = userId;
this.userName = userName;
this.password = password;
this.phone = phone;
}
private Integer userId;
private String userName;
private String password;
private String phone;
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", password='" + password + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
- 测试代码如下:
@SpringBootTest
class ProducerTest {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Test
void redisConfig() {
redisTemplate.opsForValue().set("user1",new User(1,"yj","123","111"));
User user1 = (User)redisTemplate.opsForValue().get("user1");
Assert.assertNotNull(user1);
}
@Test
void produce(){
redisTemplate.convertAndSend("demo-channel",new User(1,"yj","123","111"));
}
}