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 | 22 | <maven.compiler.target>${java.version}</maven.compiler.target> |
| 23 | 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 | 26 | <!-- Web 相关 --> |
| 27 | 27 | <knife4j.version>3.0.2</knife4j.version> |
| 28 | 28 | <swagger-annotations.version>1.5.22</swagger-annotations.version> |
| 29 | 29 | <!-- DB 相关 --> |
| 30 | 30 | <mysql-connector-java.version>5.1.46</mysql-connector-java.version> |
| 31 | 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 | 34 | <!-- Config 配置中心相关 --> |
| 35 | 35 | <apollo.version>1.7.0</apollo.version> |
| 36 | 36 | <!-- 服务保障相关 --> |
| ... | ... | @@ -42,7 +42,7 @@ |
| 42 | 42 | <!-- 工具类相关 --> |
| 43 | 43 | <lombok.version>1.16.14</lombok.version> |
| 44 | 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 | 46 | <easyexcel.verion>2.2.7</easyexcel.verion> |
| 47 | 47 | <velocity.version>2.2</velocity.version> |
| 48 | 48 | <screw.version>1.0.5</screw.version> |
| ... | ... | @@ -249,27 +249,7 @@ |
| 249 | 249 | |
| 250 | 250 | <dependency> |
| 251 | 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 | 253 | <version>${hutool.version}</version> |
| 274 | 254 | </dependency> |
| 275 | 255 | ... | ... |
src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
| 1 | 1 | package cn.iocoder.dashboard.framework.redis.config; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.net.NetUtil; | |
| 3 | +import cn.hutool.system.SystemUtil; | |
| 4 | 4 | import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; |
| 5 | 5 | import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; |
| 6 | 6 | import lombok.extern.slf4j.Slf4j; |
| 7 | 7 | import org.springframework.context.annotation.Bean; |
| 8 | 8 | import org.springframework.context.annotation.Configuration; |
| 9 | 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 | 14 | import org.springframework.data.redis.core.RedisTemplate; |
| 12 | 15 | import org.springframework.data.redis.listener.ChannelTopic; |
| 13 | 16 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; |
| 14 | 17 | import org.springframework.data.redis.serializer.RedisSerializer; |
| 15 | 18 | import org.springframework.data.redis.stream.StreamMessageListenerContainer; |
| 16 | -import org.springframework.util.ErrorHandler; | |
| 17 | 19 | |
| 18 | -import java.time.Duration; | |
| 19 | 20 | import java.util.List; |
| 20 | 21 | |
| 21 | 22 | /** |
| ... | ... | @@ -25,6 +26,9 @@ import java.util.List; |
| 25 | 26 | @Slf4j |
| 26 | 27 | public class RedisConfig { |
| 27 | 28 | |
| 29 | + /** | |
| 30 | + * 创建 RedisTemplate Bean,使用 JSON 序列化方式 | |
| 31 | + */ | |
| 28 | 32 | @Bean |
| 29 | 33 | public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { |
| 30 | 34 | // 创建 RedisTemplate 对象 |
| ... | ... | @@ -33,11 +37,16 @@ public class RedisConfig { |
| 33 | 37 | template.setConnectionFactory(factory); |
| 34 | 38 | // 使用 String 序列化方式,序列化 KEY 。 |
| 35 | 39 | template.setKeySerializer(RedisSerializer.string()); |
| 40 | + template.setHashKeySerializer(RedisSerializer.string()); | |
| 36 | 41 | // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 |
| 37 | 42 | template.setValueSerializer(RedisSerializer.json()); |
| 43 | + template.setHashValueSerializer(RedisSerializer.json()); | |
| 38 | 44 | return template; |
| 39 | 45 | } |
| 40 | 46 | |
| 47 | + /** | |
| 48 | + * 创建 Redis Pub/Sub 广播消费的容器 | |
| 49 | + */ | |
| 41 | 50 | @Bean |
| 42 | 51 | public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory, |
| 43 | 52 | List<AbstractChannelMessageListener<?>> listeners) { |
| ... | ... | @@ -54,52 +63,48 @@ public class RedisConfig { |
| 54 | 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 | 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 | 89 | try { |
| 93 | 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 | 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 | 46 | @Override |
| 47 | 47 | public void onMessage(ObjectRecord<String, String> message) { |
| 48 | 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 | 31 | * @param message 消息 |
| 32 | 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 | 35 | return redisTemplate.opsForStream().add(StreamRecords.newRecord() |
| 36 | 36 | .ofObject(JsonUtils.toJsonString(message)) // 设置内容 |
| 37 | 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 | 9 | import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; |
| 10 | 10 | import org.junit.jupiter.api.Test; |
| 11 | 11 | import org.springframework.context.annotation.Import; |
| 12 | +import org.springframework.data.redis.core.RedisTemplate; | |
| 12 | 13 | import org.springframework.data.redis.core.StringRedisTemplate; |
| 13 | 14 | |
| 14 | 15 | import javax.annotation.Resource; |
| ... | ... | @@ -31,13 +32,18 @@ public class RedisStreamTest { |
| 31 | 32 | @Resource |
| 32 | 33 | private StringRedisTemplate stringRedisTemplate; |
| 33 | 34 | |
| 35 | + @Resource | |
| 36 | + private RedisTemplate<String, Object> redisTemplate; | |
| 37 | + | |
| 34 | 38 | @Test |
| 35 | 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 | 49 | @Test | ... | ... |