1. 升级 springboot 到最新的版本,解决 spring data redis 存储的 bug
2. 梳理 StreamMessageListenerContainer Bean 的创建
This commit is contained in:
parent
be3fac7542
commit
9c76fd4b69
30
pom.xml
30
pom.xml
|
@ -22,15 +22,15 @@
|
||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
|
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
|
||||||
<!-- 统一依赖管理 -->
|
<!-- 统一依赖管理 -->
|
||||||
<spring.boot.version>2.4.2</spring.boot.version>
|
<spring.boot.version>2.4.4</spring.boot.version>
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<knife4j.version>3.0.2</knife4j.version>
|
<knife4j.version>3.0.2</knife4j.version>
|
||||||
<swagger-annotations.version>1.5.22</swagger-annotations.version>
|
<swagger-annotations.version>1.5.22</swagger-annotations.version>
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
<mysql-connector-java.version>5.1.46</mysql-connector-java.version>
|
<mysql-connector-java.version>5.1.46</mysql-connector-java.version>
|
||||||
<druid.version>1.2.4</druid.version>
|
<druid.version>1.2.4</druid.version>
|
||||||
<mybatis-plus.version>3.4.1</mybatis-plus.version>
|
<mybatis-plus.version>3.4.2</mybatis-plus.version>
|
||||||
<redisson.version>3.14.1</redisson.version>
|
<redisson.version>3.15.1</redisson.version>
|
||||||
<!-- Config 配置中心相关 -->
|
<!-- Config 配置中心相关 -->
|
||||||
<apollo.version>1.7.0</apollo.version>
|
<apollo.version>1.7.0</apollo.version>
|
||||||
<!-- 服务保障相关 -->
|
<!-- 服务保障相关 -->
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<!-- 工具类相关 -->
|
<!-- 工具类相关 -->
|
||||||
<lombok.version>1.16.14</lombok.version>
|
<lombok.version>1.16.14</lombok.version>
|
||||||
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
||||||
<hutool.version>5.5.6</hutool.version>
|
<hutool.version>5.6.1</hutool.version>
|
||||||
<easyexcel.verion>2.2.7</easyexcel.verion>
|
<easyexcel.verion>2.2.7</easyexcel.verion>
|
||||||
<velocity.version>2.2</velocity.version>
|
<velocity.version>2.2</velocity.version>
|
||||||
<screw.version>1.0.5</screw.version>
|
<screw.version>1.0.5</screw.version>
|
||||||
|
@ -249,27 +249,7 @@
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-core</artifactId>
|
<artifactId>hutool-all</artifactId>
|
||||||
<version>${hutool.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-extra</artifactId>
|
|
||||||
<version>${hutool.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-captcha</artifactId>
|
|
||||||
<version>${hutool.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-http</artifactId>
|
|
||||||
<version>${hutool.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-crypto</artifactId>
|
|
||||||
<version>${hutool.version}</version>
|
<version>${hutool.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
package cn.iocoder.dashboard.framework.redis.config;
|
package cn.iocoder.dashboard.framework.redis.config;
|
||||||
|
|
||||||
import cn.hutool.core.net.NetUtil;
|
import cn.hutool.system.SystemUtil;
|
||||||
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
|
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
|
||||||
import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
|
import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
import org.springframework.data.redis.connection.stream.*;
|
import org.springframework.data.redis.connection.stream.Consumer;
|
||||||
|
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||||
|
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||||
|
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.listener.ChannelTopic;
|
import org.springframework.data.redis.listener.ChannelTopic;
|
||||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
|
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
|
||||||
import org.springframework.util.ErrorHandler;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,6 +26,9 @@ import java.util.List;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RedisConfig {
|
public class RedisConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 RedisTemplate Bean,使用 JSON 序列化方式
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||||
// 创建 RedisTemplate 对象
|
// 创建 RedisTemplate 对象
|
||||||
|
@ -33,11 +37,16 @@ public class RedisConfig {
|
||||||
template.setConnectionFactory(factory);
|
template.setConnectionFactory(factory);
|
||||||
// 使用 String 序列化方式,序列化 KEY 。
|
// 使用 String 序列化方式,序列化 KEY 。
|
||||||
template.setKeySerializer(RedisSerializer.string());
|
template.setKeySerializer(RedisSerializer.string());
|
||||||
|
template.setHashKeySerializer(RedisSerializer.string());
|
||||||
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
|
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
|
||||||
template.setValueSerializer(RedisSerializer.json());
|
template.setValueSerializer(RedisSerializer.json());
|
||||||
|
template.setHashValueSerializer(RedisSerializer.json());
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Redis Pub/Sub 广播消费的容器
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
|
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
|
||||||
List<AbstractChannelMessageListener<?>> listeners) {
|
List<AbstractChannelMessageListener<?>> listeners) {
|
||||||
|
@ -54,52 +63,48 @@ public class RedisConfig {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Redis Stream 集群消费的容器
|
||||||
|
*
|
||||||
|
* Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
|
||||||
|
*/
|
||||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||||
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
|
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(RedisTemplate<String, Object> redisTemplate,
|
||||||
RedisConnectionFactory factory, List<AbstractStreamMessageListener<?>> listeners) {
|
List<AbstractStreamMessageListener<?>> listeners) {
|
||||||
// 创建配置对象
|
// 第一步,创建 StreamMessageListenerContainer 容器
|
||||||
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>>
|
// 创建 options 配置
|
||||||
streamMessageListenerContainerOptions = StreamMessageListenerContainer.StreamMessageListenerContainerOptions
|
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
|
||||||
.builder()
|
StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
|
||||||
// 一次性最多拉取多少条消息
|
.batchSize(10) // 一次性最多拉取多少条消息
|
||||||
.batchSize(10)
|
.targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
|
||||||
// 执行消息轮询的执行器
|
.build();
|
||||||
// .executor(this.threadPoolTaskExecutor)
|
// 创建 container 对象
|
||||||
// 消息消费异常的handler
|
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create(
|
||||||
.errorHandler(new ErrorHandler() {
|
redisTemplate.getRequiredConnectionFactory(), containerOptions);
|
||||||
@Override
|
|
||||||
public void handleError(Throwable t) {
|
|
||||||
// throw new RuntimeException(t);
|
|
||||||
t.printStackTrace();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 超时时间,设置为0,表示不超时(超时后会抛出异常)
|
|
||||||
.pollTimeout(Duration.ZERO)
|
|
||||||
// 序列化器
|
|
||||||
.serializer(RedisSerializer.string())
|
|
||||||
.targetType(String.class)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 根据配置对象创建监听容器对象
|
// 第二步,注册监听器,消费对应的 Stream 主题
|
||||||
StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer
|
String consumerName = buildConsumerName();
|
||||||
.create(factory, streamMessageListenerContainerOptions);
|
listeners.forEach(listener -> {
|
||||||
|
// 创建 listener 对应的消费者分组
|
||||||
RedisTemplate<String, Object> redisTemplate = redisTemplate(factory);
|
|
||||||
|
|
||||||
// 使用监听容器对象开始监听消费(使用的是手动确认方式)
|
|
||||||
String consumerName = NetUtil.getLocalHostName(); // TODO 需要优化下,晚点参考下 rocketmq consumer 的
|
|
||||||
for (AbstractStreamMessageListener<?> listener : listeners) {
|
|
||||||
try {
|
try {
|
||||||
redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
|
redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {}
|
||||||
// ignore.printStackTrace();
|
// 创建 Consumer 对象
|
||||||
}
|
Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
|
||||||
|
// 设置 Consumer 消费进度,以最小消费进度为准
|
||||||
container.receive(Consumer.from(listener.getGroup(), consumerName),
|
StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
|
||||||
StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()), listener);
|
// 设置 Consumer 监听
|
||||||
}
|
StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
|
||||||
|
.builder(streamOffset).consumer(consumer)
|
||||||
|
.autoAcknowledge(false) // 不自动 ack
|
||||||
|
.cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
|
||||||
|
container.register(builder.build(), listener);
|
||||||
|
});
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String buildConsumerName() {
|
||||||
|
return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ public abstract class AbstractStreamMessageListener<T extends StreamMessage>
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(ObjectRecord<String, String> message) {
|
public void onMessage(ObjectRecord<String, String> message) {
|
||||||
System.out.println(message);
|
System.out.println(message);
|
||||||
|
if (true) {
|
||||||
|
// throw new IllegalStateException("测试下");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -31,7 +31,7 @@ public class RedisMessageUtils {
|
||||||
* @param message 消息
|
* @param message 消息
|
||||||
* @return 消息记录的编号对象
|
* @return 消息记录的编号对象
|
||||||
*/
|
*/
|
||||||
public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, String> redisTemplate, T message) {
|
public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, ?> redisTemplate, T message) {
|
||||||
return redisTemplate.opsForStream().add(StreamRecords.newRecord()
|
return redisTemplate.opsForStream().add(StreamRecords.newRecord()
|
||||||
.ofObject(JsonUtils.toJsonString(message)) // 设置内容
|
.ofObject(JsonUtils.toJsonString(message)) // 设置内容
|
||||||
.withStreamKey(message.getStreamKey())); // 设置 stream key
|
.withStreamKey(message.getStreamKey())); // 设置 stream key
|
||||||
|
|
|
@ -9,6 +9,7 @@ import cn.iocoder.dashboard.modules.system.mq.message.mail.SysMailSendMessage;
|
||||||
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
|
import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
@ -31,13 +32,18 @@ public class RedisStreamTest {
|
||||||
@Resource
|
@Resource
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProducer01() {
|
public void testProducer01() {
|
||||||
// 创建消息
|
for (int i = 0; i < 100; i++) {
|
||||||
SysSmsSendMessage message = new SysSmsSendMessage();
|
// 创建消息
|
||||||
message.setMobile("15601691300").setTemplateCode("test");
|
SysSmsSendMessage message = new SysSmsSendMessage();
|
||||||
// 发送消息
|
message.setMobile("15601691300").setTemplateCode("test:" + i);
|
||||||
RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
|
// 发送消息
|
||||||
|
RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in New Issue