Commit 9c76fd4b69110a5ad802dd77ed89fc31d5364473
1 parent
be3fac75
1. 升级 springboot 到最新的版本,解决 spring data redis 存储的 bug
2. 梳理 StreamMessageListenerContainer Bean 的创建
Showing
5 changed files
with
70 additions
and
76 deletions
pom.xml
| @@ -22,15 +22,15 @@ | @@ -22,15 +22,15 @@ | ||
| 22 | <maven.compiler.target>${java.version}</maven.compiler.target> | 22 | <maven.compiler.target>${java.version}</maven.compiler.target> |
| 23 | <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version> | 23 | <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version> |
| 24 | <!-- 统一依赖管理 --> | 24 | <!-- 统一依赖管理 --> |
| 25 | - <spring.boot.version>2.4.2</spring.boot.version> | 25 | + <spring.boot.version>2.4.4</spring.boot.version> |
| 26 | <!-- Web 相关 --> | 26 | <!-- Web 相关 --> |
| 27 | <knife4j.version>3.0.2</knife4j.version> | 27 | <knife4j.version>3.0.2</knife4j.version> |
| 28 | <swagger-annotations.version>1.5.22</swagger-annotations.version> | 28 | <swagger-annotations.version>1.5.22</swagger-annotations.version> |
| 29 | <!-- DB 相关 --> | 29 | <!-- DB 相关 --> |
| 30 | <mysql-connector-java.version>5.1.46</mysql-connector-java.version> | 30 | <mysql-connector-java.version>5.1.46</mysql-connector-java.version> |
| 31 | <druid.version>1.2.4</druid.version> | 31 | <druid.version>1.2.4</druid.version> |
| 32 | - <mybatis-plus.version>3.4.1</mybatis-plus.version> | ||
| 33 | - <redisson.version>3.14.1</redisson.version> | 32 | + <mybatis-plus.version>3.4.2</mybatis-plus.version> |
| 33 | + <redisson.version>3.15.1</redisson.version> | ||
| 34 | <!-- Config 配置中心相关 --> | 34 | <!-- Config 配置中心相关 --> |
| 35 | <apollo.version>1.7.0</apollo.version> | 35 | <apollo.version>1.7.0</apollo.version> |
| 36 | <!-- 服务保障相关 --> | 36 | <!-- 服务保障相关 --> |
| @@ -42,7 +42,7 @@ | @@ -42,7 +42,7 @@ | ||
| 42 | <!-- 工具类相关 --> | 42 | <!-- 工具类相关 --> |
| 43 | <lombok.version>1.16.14</lombok.version> | 43 | <lombok.version>1.16.14</lombok.version> |
| 44 | <mapstruct.version>1.4.1.Final</mapstruct.version> | 44 | <mapstruct.version>1.4.1.Final</mapstruct.version> |
| 45 | - <hutool.version>5.5.6</hutool.version> | 45 | + <hutool.version>5.6.1</hutool.version> |
| 46 | <easyexcel.verion>2.2.7</easyexcel.verion> | 46 | <easyexcel.verion>2.2.7</easyexcel.verion> |
| 47 | <velocity.version>2.2</velocity.version> | 47 | <velocity.version>2.2</velocity.version> |
| 48 | <screw.version>1.0.5</screw.version> | 48 | <screw.version>1.0.5</screw.version> |
| @@ -249,27 +249,7 @@ | @@ -249,27 +249,7 @@ | ||
| 249 | 249 | ||
| 250 | <dependency> | 250 | <dependency> |
| 251 | <groupId>cn.hutool</groupId> | 251 | <groupId>cn.hutool</groupId> |
| 252 | - <artifactId>hutool-core</artifactId> | ||
| 253 | - <version>${hutool.version}</version> | ||
| 254 | - </dependency> | ||
| 255 | - <dependency> | ||
| 256 | - <groupId>cn.hutool</groupId> | ||
| 257 | - <artifactId>hutool-extra</artifactId> | ||
| 258 | - <version>${hutool.version}</version> | ||
| 259 | - </dependency> | ||
| 260 | - <dependency> | ||
| 261 | - <groupId>cn.hutool</groupId> | ||
| 262 | - <artifactId>hutool-captcha</artifactId> | ||
| 263 | - <version>${hutool.version}</version> | ||
| 264 | - </dependency> | ||
| 265 | - <dependency> | ||
| 266 | - <groupId>cn.hutool</groupId> | ||
| 267 | - <artifactId>hutool-http</artifactId> | ||
| 268 | - <version>${hutool.version}</version> | ||
| 269 | - </dependency> | ||
| 270 | - <dependency> | ||
| 271 | - <groupId>cn.hutool</groupId> | ||
| 272 | - <artifactId>hutool-crypto</artifactId> | 252 | + <artifactId>hutool-all</artifactId> |
| 273 | <version>${hutool.version}</version> | 253 | <version>${hutool.version}</version> |
| 274 | </dependency> | 254 | </dependency> |
| 275 | 255 |
src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
| 1 | package cn.iocoder.dashboard.framework.redis.config; | 1 | package cn.iocoder.dashboard.framework.redis.config; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.net.NetUtil; | 3 | +import cn.hutool.system.SystemUtil; |
| 4 | import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; | 4 | import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; |
| 5 | import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; | 5 | import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; |
| 6 | import lombok.extern.slf4j.Slf4j; | 6 | import lombok.extern.slf4j.Slf4j; |
| 7 | import org.springframework.context.annotation.Bean; | 7 | import org.springframework.context.annotation.Bean; |
| 8 | import org.springframework.context.annotation.Configuration; | 8 | import org.springframework.context.annotation.Configuration; |
| 9 | import org.springframework.data.redis.connection.RedisConnectionFactory; | 9 | import org.springframework.data.redis.connection.RedisConnectionFactory; |
| 10 | -import org.springframework.data.redis.connection.stream.*; | 10 | +import org.springframework.data.redis.connection.stream.Consumer; |
| 11 | +import org.springframework.data.redis.connection.stream.ObjectRecord; | ||
| 12 | +import org.springframework.data.redis.connection.stream.ReadOffset; | ||
| 13 | +import org.springframework.data.redis.connection.stream.StreamOffset; | ||
| 11 | import org.springframework.data.redis.core.RedisTemplate; | 14 | import org.springframework.data.redis.core.RedisTemplate; |
| 12 | import org.springframework.data.redis.listener.ChannelTopic; | 15 | import org.springframework.data.redis.listener.ChannelTopic; |
| 13 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; | 16 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; |
| 14 | import org.springframework.data.redis.serializer.RedisSerializer; | 17 | import org.springframework.data.redis.serializer.RedisSerializer; |
| 15 | import org.springframework.data.redis.stream.StreamMessageListenerContainer; | 18 | import org.springframework.data.redis.stream.StreamMessageListenerContainer; |
| 16 | -import org.springframework.util.ErrorHandler; | ||
| 17 | 19 | ||
| 18 | -import java.time.Duration; | ||
| 19 | import java.util.List; | 20 | import java.util.List; |
| 20 | 21 | ||
| 21 | /** | 22 | /** |
| @@ -25,6 +26,9 @@ import java.util.List; | @@ -25,6 +26,9 @@ import java.util.List; | ||
| 25 | @Slf4j | 26 | @Slf4j |
| 26 | public class RedisConfig { | 27 | public class RedisConfig { |
| 27 | 28 | ||
| 29 | + /** | ||
| 30 | + * 创建 RedisTemplate Bean,使用 JSON 序列化方式 | ||
| 31 | + */ | ||
| 28 | @Bean | 32 | @Bean |
| 29 | public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { | 33 | public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { |
| 30 | // 创建 RedisTemplate 对象 | 34 | // 创建 RedisTemplate 对象 |
| @@ -33,11 +37,16 @@ public class RedisConfig { | @@ -33,11 +37,16 @@ public class RedisConfig { | ||
| 33 | template.setConnectionFactory(factory); | 37 | template.setConnectionFactory(factory); |
| 34 | // 使用 String 序列化方式,序列化 KEY 。 | 38 | // 使用 String 序列化方式,序列化 KEY 。 |
| 35 | template.setKeySerializer(RedisSerializer.string()); | 39 | template.setKeySerializer(RedisSerializer.string()); |
| 40 | + template.setHashKeySerializer(RedisSerializer.string()); | ||
| 36 | // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 | 41 | // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 |
| 37 | template.setValueSerializer(RedisSerializer.json()); | 42 | template.setValueSerializer(RedisSerializer.json()); |
| 43 | + template.setHashValueSerializer(RedisSerializer.json()); | ||
| 38 | return template; | 44 | return template; |
| 39 | } | 45 | } |
| 40 | 46 | ||
| 47 | + /** | ||
| 48 | + * 创建 Redis Pub/Sub 广播消费的容器 | ||
| 49 | + */ | ||
| 41 | @Bean | 50 | @Bean |
| 42 | public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory, | 51 | public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory, |
| 43 | List<AbstractChannelMessageListener<?>> listeners) { | 52 | List<AbstractChannelMessageListener<?>> listeners) { |
| @@ -54,52 +63,48 @@ public class RedisConfig { | @@ -54,52 +63,48 @@ public class RedisConfig { | ||
| 54 | return container; | 63 | return container; |
| 55 | } | 64 | } |
| 56 | 65 | ||
| 66 | + /** | ||
| 67 | + * 创建 Redis Stream 集群消费的容器 | ||
| 68 | + * | ||
| 69 | + * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html | ||
| 70 | + */ | ||
| 57 | @Bean(initMethod = "start", destroyMethod = "stop") | 71 | @Bean(initMethod = "start", destroyMethod = "stop") |
| 58 | - public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer( | ||
| 59 | - RedisConnectionFactory factory, List<AbstractStreamMessageListener<?>> listeners) { | ||
| 60 | - // 创建配置对象 | ||
| 61 | - StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> | ||
| 62 | - streamMessageListenerContainerOptions = StreamMessageListenerContainer.StreamMessageListenerContainerOptions | ||
| 63 | - .builder() | ||
| 64 | - // 一次性最多拉取多少条消息 | ||
| 65 | - .batchSize(10) | ||
| 66 | - // 执行消息轮询的执行器 | ||
| 67 | - // .executor(this.threadPoolTaskExecutor) | ||
| 68 | - // 消息消费异常的handler | ||
| 69 | - .errorHandler(new ErrorHandler() { | ||
| 70 | - @Override | ||
| 71 | - public void handleError(Throwable t) { | ||
| 72 | - // throw new RuntimeException(t); | ||
| 73 | - t.printStackTrace(); | ||
| 74 | - } | ||
| 75 | - }) | ||
| 76 | - // 超时时间,设置为0,表示不超时(超时后会抛出异常) | ||
| 77 | - .pollTimeout(Duration.ZERO) | ||
| 78 | - // 序列化器 | ||
| 79 | - .serializer(RedisSerializer.string()) | ||
| 80 | - .targetType(String.class) | ||
| 81 | - .build(); | 72 | + public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(RedisTemplate<String, Object> redisTemplate, |
| 73 | + List<AbstractStreamMessageListener<?>> listeners) { | ||
| 74 | + // 第一步,创建 StreamMessageListenerContainer 容器 | ||
| 75 | + // 创建 options 配置 | ||
| 76 | + StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions = | ||
| 77 | + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() | ||
| 78 | + .batchSize(10) // 一次性最多拉取多少条消息 | ||
| 79 | + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 | ||
| 80 | + .build(); | ||
| 81 | + // 创建 container 对象 | ||
| 82 | + StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create( | ||
| 83 | + redisTemplate.getRequiredConnectionFactory(), containerOptions); | ||
| 82 | 84 | ||
| 83 | - // 根据配置对象创建监听容器对象 | ||
| 84 | - StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer | ||
| 85 | - .create(factory, streamMessageListenerContainerOptions); | ||
| 86 | - | ||
| 87 | - RedisTemplate<String, Object> redisTemplate = redisTemplate(factory); | ||
| 88 | - | ||
| 89 | - // 使用监听容器对象开始监听消费(使用的是手动确认方式) | ||
| 90 | - String consumerName = NetUtil.getLocalHostName(); // TODO 需要优化下,晚点参考下 rocketmq consumer 的 | ||
| 91 | - for (AbstractStreamMessageListener<?> listener : listeners) { | 85 | + // 第二步,注册监听器,消费对应的 Stream 主题 |
| 86 | + String consumerName = buildConsumerName(); | ||
| 87 | + listeners.forEach(listener -> { | ||
| 88 | + // 创建 listener 对应的消费者分组 | ||
| 92 | try { | 89 | try { |
| 93 | redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); | 90 | redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); |
| 94 | - } catch (Exception ignore) { | ||
| 95 | -// ignore.printStackTrace(); | ||
| 96 | - } | ||
| 97 | - | ||
| 98 | - container.receive(Consumer.from(listener.getGroup(), consumerName), | ||
| 99 | - StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()), listener); | ||
| 100 | - } | ||
| 101 | - | 91 | + } catch (Exception ignore) {} |
| 92 | + // 创建 Consumer 对象 | ||
| 93 | + Consumer consumer = Consumer.from(listener.getGroup(), consumerName); | ||
| 94 | + // 设置 Consumer 消费进度,以最小消费进度为准 | ||
| 95 | + StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); | ||
| 96 | + // 设置 Consumer 监听 | ||
| 97 | + StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest | ||
| 98 | + .builder(streamOffset).consumer(consumer) | ||
| 99 | + .autoAcknowledge(false) // 不自动 ack | ||
| 100 | + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false | ||
| 101 | + container.register(builder.build(), listener); | ||
| 102 | + }); | ||
| 102 | return container; | 103 | return container; |
| 103 | } | 104 | } |
| 104 | 105 | ||
| 106 | + private static String buildConsumerName() { | ||
| 107 | + return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); | ||
| 108 | + } | ||
| 109 | + | ||
| 105 | } | 110 | } |
src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java
| @@ -46,6 +46,9 @@ public abstract class AbstractStreamMessageListener<T extends StreamMessage> | @@ -46,6 +46,9 @@ public abstract class AbstractStreamMessageListener<T extends StreamMessage> | ||
| 46 | @Override | 46 | @Override |
| 47 | public void onMessage(ObjectRecord<String, String> message) { | 47 | public void onMessage(ObjectRecord<String, String> message) { |
| 48 | System.out.println(message); | 48 | System.out.println(message); |
| 49 | + if (true) { | ||
| 50 | +// throw new IllegalStateException("测试下"); | ||
| 51 | + } | ||
| 49 | } | 52 | } |
| 50 | 53 | ||
| 51 | /** | 54 | /** |
src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java
| @@ -31,7 +31,7 @@ public class RedisMessageUtils { | @@ -31,7 +31,7 @@ public class RedisMessageUtils { | ||
| 31 | * @param message 消息 | 31 | * @param message 消息 |
| 32 | * @return 消息记录的编号对象 | 32 | * @return 消息记录的编号对象 |
| 33 | */ | 33 | */ |
| 34 | - public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, String> redisTemplate, T message) { | 34 | + public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, ?> redisTemplate, T message) { |
| 35 | return redisTemplate.opsForStream().add(StreamRecords.newRecord() | 35 | return redisTemplate.opsForStream().add(StreamRecords.newRecord() |
| 36 | .ofObject(JsonUtils.toJsonString(message)) // 设置内容 | 36 | .ofObject(JsonUtils.toJsonString(message)) // 设置内容 |
| 37 | .withStreamKey(message.getStreamKey())); // 设置 stream key | 37 | .withStreamKey(message.getStreamKey())); // 设置 stream key |
src/test-integration/java/cn/iocoder/dashboard/framework/redis/core/stream/RedisStreamTest.java
| @@ -9,6 +9,7 @@ import cn.iocoder.dashboard.modules.system.mq.message.mail.SysMailSendMessage; | @@ -9,6 +9,7 @@ import cn.iocoder.dashboard.modules.system.mq.message.mail.SysMailSendMessage; | ||
| 9 | import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; | 9 | import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; |
| 10 | import org.junit.jupiter.api.Test; | 10 | import org.junit.jupiter.api.Test; |
| 11 | import org.springframework.context.annotation.Import; | 11 | import org.springframework.context.annotation.Import; |
| 12 | +import org.springframework.data.redis.core.RedisTemplate; | ||
| 12 | import org.springframework.data.redis.core.StringRedisTemplate; | 13 | import org.springframework.data.redis.core.StringRedisTemplate; |
| 13 | 14 | ||
| 14 | import javax.annotation.Resource; | 15 | import javax.annotation.Resource; |
| @@ -31,13 +32,18 @@ public class RedisStreamTest { | @@ -31,13 +32,18 @@ public class RedisStreamTest { | ||
| 31 | @Resource | 32 | @Resource |
| 32 | private StringRedisTemplate stringRedisTemplate; | 33 | private StringRedisTemplate stringRedisTemplate; |
| 33 | 34 | ||
| 35 | + @Resource | ||
| 36 | + private RedisTemplate<String, Object> redisTemplate; | ||
| 37 | + | ||
| 34 | @Test | 38 | @Test |
| 35 | public void testProducer01() { | 39 | public void testProducer01() { |
| 36 | - // 创建消息 | ||
| 37 | - SysSmsSendMessage message = new SysSmsSendMessage(); | ||
| 38 | - message.setMobile("15601691300").setTemplateCode("test"); | ||
| 39 | - // 发送消息 | ||
| 40 | - RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message); | 40 | + for (int i = 0; i < 100; i++) { |
| 41 | + // 创建消息 | ||
| 42 | + SysSmsSendMessage message = new SysSmsSendMessage(); | ||
| 43 | + message.setMobile("15601691300").setTemplateCode("test:" + i); | ||
| 44 | + // 发送消息 | ||
| 45 | + RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message); | ||
| 46 | + } | ||
| 41 | } | 47 | } |
| 42 | 48 | ||
| 43 | @Test | 49 | @Test |