本篇文章主要来讲Spring Boot 整合Redis实现消息队列,实现redis用作消息队列有多种方式,比如:
不过这里讲的是Pub/Sub 机制的,这种方式优缺点大致如下:
优点:
缺点:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.0version>
<relativePath/>
parent>
<groupId>com.aliangroupId>
<artifactId>redis-message-queueartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>redis-message-queuename>
<description>redis-message-queuedescription>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<project.package.directory>targetproject.package.directory>
<java.version>1.8java.version>
<jackson.version>2.9.10jackson.version>
<lombok.version>1.16.14lombok.version>
<fastjson.version>1.2.68fastjson.version>
<junit.version>4.12junit.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>${jackson.version}version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatypegroupId>
<artifactId>jackson-datatype-jsr310artifactId>
<version>${jackson.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.properties
# 端口
server.port=8090
# 上下文路径
server.servlet.context-path=/redisQueue
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
#spring.redis.host=192.168.0.193
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=20
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=10
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=20000
# 读时间(毫秒)
spring.redis.timeout=10000
# 连接超时时间(毫秒)
spring.redis.connect-timeout=10000
RedisConfiguration.java
package com.alian.queue.config;
import com.alian.queue.listener.RedisMessageListenerListener;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.EnableCaching;
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.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@Configuration
@EnableCaching
public class RedisConfiguration {
/**
* redis配置
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 实例化redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// key采用String的序列化
redisTemplate.setKeySerializer(keySerializer());
// value采用jackson序列化
redisTemplate.setValueSerializer(valueSerializer());
// Hash key采用String的序列化
redisTemplate.setHashKeySerializer(keySerializer());
// Hash value采用jackson序列化
redisTemplate.setHashValueSerializer(valueSerializer());
//执行函数,初始化RedisTemplate
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public ChannelTopic channelTopic() {
return new ChannelTopic("TOPIC_USER");
}
@Bean
public RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, RedisMessageListenerListener redisMessageListenerListener) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(redisMessageListenerListener, channelTopic());
return redisMessageListenerContainer;
}
/**
* key类型采用String序列化
*
* @return
*/
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
/**
* value采用JSON序列化
*
* @return
*/
private RedisSerializer<Object> valueSerializer() {
//设置jackson序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//设置序列化对象
jackson2JsonRedisSerializer.setObjectMapper(getMapper());
return jackson2JsonRedisSerializer;
}
/**
* 使用com.fasterxml.jackson.databind.ObjectMapper
* 对数据进行处理包括java8里的时间
*
* @return
*/
private ObjectMapper getMapper() {
ObjectMapper mapper = new ObjectMapper();
//设置可见性
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//默认键入对象
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//设置Java 8 时间序列化
JavaTimeModule timeModule = new JavaTimeModule();
timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
timeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
//禁用把时间转为时间戳
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(timeModule);
return mapper;
}
}
这里就是在整合redis的前提下(如果不懂可以参考:SpringBoot整合redis(redis支持单节点和集群)),然后新增了如下配置:
@Bean
public ChannelTopic channelTopic() {
return new ChannelTopic("TOPIC_USER");
}
@Bean
public RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, RedisMessageListenerListener redisMessageListenerListener) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(redisMessageListenerListener, channelTopic());
return redisMessageListenerContainer;
}
RedisMessageListenerContainer 是为Redis消息侦听器 MessageListener 提供异步行为的容器。处理侦听、转换和消息分派的低级别详细信息。它与低级别Redis(每个订阅一个连接)相反,容器只使用一个连接,该连接对所有注册的侦听器都是“多路复用”的,消息调度是通过任务执行器完成的。容器以惰性方式使用连接(仅当至少配置了一个侦听器时才使用连接),同时添加和删除侦听器具有未定义的结果,强烈建议对这些方法进行相应的同步/排序。
我这里使用的是主题订阅:ChannelTopic,你也可以使用模式匹配:PatternTopic,从而匹配多个信道。
package com.alian.queue.listener;
import com.alian.queue.service.ConsumeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisMessageListenerListener implements MessageListener {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ConsumeService consumeService;
/**
* 消息处理
*
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(pattern);
log.info("onMessage --> 消息通道是:{}", channel);
try {
RedisSerializer<?> valueSerializer = redisTemplate.getValueSerializer();
Object deserialize = valueSerializer.deserialize(message.getBody());
log.info("反序列化的结果:{}", deserialize);
if (deserialize == null) {
return;
}
String md5DigestAsHex = DigestUtils.md5DigestAsHex(deserialize.toString().getBytes(StandardCharsets.UTF_8));
log.info("计算得到的key: {}", md5DigestAsHex);
Boolean result = redisTemplate.opsForValue().setIfAbsent(md5DigestAsHex, "1", 20, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
// redis消息进行处理
consumeService.processMessage(channel, deserialize.toString());
log.info("处理redis消息完成");
} else {
log.info("其他服务处理中");
}
} catch (Exception e) {
e.printStackTrace();
log.error("处理redis消息异常:", e);
}
}
}
我们实现MessageListener 接口,就可以通过onMessage()方法接收到消息了,该方法有两个参数:
ConsumeService.java
package com.alian.queue.service;
import com.alian.queue.dto.UserDto;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ConsumeService {
public void processMessage(String channel,String message) {
// 可以根据channel再继续映射到不同的实现
UserDto userDto = JSONObject.parseObject(message, UserDto.class);
log.info("接收的结果:{}", userDto);
// 做业务...
// 还可以分布式锁幂等处理
}
}
这个就是消息处理了,可以根据channel再继续映射到不同的实现,然后业务也可以继续使用分布式锁进行逻辑判断处理,这里就不具体去操作了。
UserDto.java
package com.alian.queue.dto;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class UserDto implements Serializable {
private String id;//员工ID
private String name;//员工姓名
private int age;//员工年龄
private String department;//部门
private double salary;//工资
private LocalDateTime hireDate;//入职时间
public UserDto() {
}
/*
* 简单的构造方法用于测试
*/
public UserDto(String id, String name, int age, String department, double salary, LocalDateTime hireDate) {
this.id = id;
this.name = name;
this.age = age;
this.department = department;
this.salary = salary;
this.hireDate = hireDate;
}
}
我们把上面的服务启动多个实例,这里就用端口区别,分别是 8090 和 8091,然后发送消息到 Redis,下面使我们的测试类:
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisMessageQueueTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void sendMessage() {
UserDto userDto = new UserDto("BAT002", "包雅馨", 25, "财务部", 8800.0, LocalDateTime.of(2016, 11, 10, 8, 30, 0));
// 注意这里的通道名【TOPIC_USER】要和RedisMessageListenerContainer里面配置的一致
redisTemplate.convertAndSend("TOPIC_USER", JSON.toJSONString(userDto));
log.info("发送成功");
}
}
注意这里的通道名要和 RedisMessageListenerContainer 里配置的一致,不然消息发送不出去的。
8090的日志
onMessage --> 消息通道是:TOPIC_USER
反序列化的结果:{"age":25,"department":"财务部","hireDate":"2016-11-10T08:30:00","id":"BAT002","name":"包雅馨","salary":8800.0}
计算得到的key: c69dc1563cc892718bf3ee0c5b90320b
接收的结果:UserDto(id=BAT002, name=包雅馨, age=25, department=财务部, salary=8800.0, hireDate=2016-11-10T08:30)
处理redis消息完成
8091的日志
onMessage --> 消息通道是:TOPIC_USER
反序列化的结果:{"age":25,"department":"财务部","hireDate":"2016-11-10T08:30:00","id":"BAT002","name":"包雅馨","salary":8800.0}
计算得到的key: c69dc1563cc892718bf3ee0c5b90320b
其他服务处理中
从结果上可以看到,分布式服务都可以接收到消息,但是最终只有一台服务会真正进行业务的处理,因为我这里使用了最简单的分布式锁来控制了,实际上 redisTemplate.opsForValue().setIfAbsent() 并不是最优解,尤其是在集群模式,我这里只是为了演示要有这么一个操作,推荐还是使用 Redisson 去做分布式锁更可靠。如果有不懂的可以参考我之前的文章:SpringBoot基于Redisson实现分布式锁并分析其原理
其实我们还可以继续去优化我们的配置,消息的接收都是在频繁的创建线程,从而占用系统资源,我们可以通过线程池的方式去优化,RedisMessageListenerContainer 类中有一个方法setTaskExecutor(Executor taskExecutor)可以为监听容器配置线程池。配置线程池以后,所有的线程都会由该线程池产生,因此我们可以通过调节线程池来控制队列监听的速率。修改步骤大致如下:
RedisConfiguration 新注册Bean:Executor
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//设置核心线程数
executor.setCorePoolSize(5);
//设置最大线程数
executor.setMaxPoolSize(10);
//设置任务队列容量
executor.setQueueCapacity(10);
//设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
//设置默认线程名称(线程前缀名称,有助于区分不同线程池之间的线程比如:taskExecutor-)
executor.setThreadNamePrefix("taskExecutor-");
//设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//设置允许核心线程超时
executor.setAllowCoreThreadTimeOut(true);
return executor;
}
redisMessageListenerContainer.setTaskExecutor(taskExecutor())
@Bean
public RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, RedisMessageListenerListener redisMessageListenerListener) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
// 设置连接工厂
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
// 绑定监听器和信道
redisMessageListenerContainer.addMessageListener(redisMessageListenerListener, channelTopic());
// 配置任务执行器
redisMessageListenerContainer.setTaskExecutor(taskExecutor());
return redisMessageListenerContainer;
}
RedisConfiguration 增加注解@EnableAsync,为了其他操作也能用到异步操作处理。
@Slf4j
@EnableAsync
@Configuration
@EnableCaching
public class RedisConfiguration {
//...
}