Commit 9c76fd4b69110a5ad802dd77ed89fc31d5364473

Authored by YunaiV
1 parent be3fac75

1. 升级 springboot 到最新的版本,解决 spring data redis 存储的 bug

2. 梳理 StreamMessageListenerContainer Bean 的创建
... ... @@ -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&lt;T extends StreamMessage&gt;
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
... ...