Commit 9c76fd4b69110a5ad802dd77ed89fc31d5364473

Authored by YunaiV
1 parent be3fac75

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

2. 梳理 StreamMessageListenerContainer Bean 的创建
@@ -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&lt;T extends StreamMessage&gt; @@ -46,6 +46,9 @@ public abstract class AbstractStreamMessageListener&lt;T extends StreamMessage&gt;
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