Files
cn-rdms/rdms-framework/rdms-spring-boot-starter-mq/README.md
2026-03-11 19:32:37 +08:00

257 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# rdms-spring-boot-starter-mq
## 模块定位
`rdms-spring-boot-starter-mq` 是项目里的消息队列基础封装,目标是:
1. 对 Redis 消息模型做统一抽象,降低业务接入复杂度
2. 通过自动配置按需启用消费者容器,不强制业务使用全部能力
3. 为后续多 MQ 方案Redis / RabbitMQ / RocketMQ / Kafka提供统一入口
当前实现里,**核心能力在 Redis**RabbitMQ 是轻量补充(消息转换器)。
## 设计思路
### 1. Redis 统一模型
Redis 相关能力分成两类:
1. `Pub/Sub` 广播模型
2. `Stream` 分组消费模型
两者的消息基类分别是:
1. `AbstractRedisChannelMessage`
2. `AbstractRedisStreamMessage`
共同父类是 `AbstractRedisMessage`,内置 `headers`,方便扩展链路信息。
### 2. 统一发送模板
发送端统一走 `RedisMQTemplate`:
1. `send(AbstractRedisChannelMessage)` -> Redis `convertAndSend`
2. `send(AbstractRedisStreamMessage)` -> Redis Stream `XADD`
这样业务代码不需要关心底层命令细节。
### 3. 监听器抽象 + 惰性自动装配
消费者由两个抽象监听器承接:
1. `AbstractRedisChannelMessageListener<T>`
2. `AbstractRedisStreamMessageListener<T>`
自动配置采用 `@ConditionalOnBean(...)`:
1. 只有你定义了对应监听器 Bean容器才会注册消费者
2. 没有监听器时不会额外启动 MQ 消费组件
### 4. Stream 运维补偿
针对 Redis Stream框架额外提供两个定时任务:
1. `RedisPendingMessageResendJob`
扫描 pending 超时消息并重投(默认超时 5 分钟)
2. `RedisStreamMessageCleanupJob`
定时 `XTRIM`,默认仅保留最近 10000 条,防止内存膨胀
两者都使用 Redisson 分布式锁,避免多实例重复执行。
### 5. RabbitMQ 轻封装
`RdmsRabbitMQAutoConfiguration` 只提供 `Jackson2JsonMessageConverter`,让 RabbitTemplate / @RabbitListener 默认按 JSON 处理消息。
### 6. 多 MQ 的真实边界
虽然模块描述写了支持 Redis / RocketMQ / RabbitMQ / Kafka
但本 starter 的自动配置主要是 Redis + Rabbit。
RocketMQ / Kafka 的业务化接入在 `rdms-spring-boot-starter-websocket` 里有更完整示例。
## 自动配置入口
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`:
1. `RdmsRedisMQProducerAutoConfiguration`
2. `RdmsRedisMQConsumerAutoConfiguration`
3. `RdmsRabbitMQAutoConfiguration`
## 如何使用
## 1. 引入依赖
一般由业务模块直接依赖:
```xml
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-mq</artifactId>
</dependency>
```
## 1.1 项目现有 Redis 作为 MQ 的常规用法(推荐)
推荐按这个顺序接入:
1. 配好 `spring.data.redis.*`
2. 先按第 2 节使用 `Pub/Sub`(这是项目里最常见路径)。
3. 需要可恢复消费时,再按第 3 节接入 `Stream`
WebSocket 场景建议:
1. 多实例广播: `rdms.websocket.sender-type: redis`
2. 单机部署: 保持 `rdms.websocket.sender-type: local`
说明:
1. 只要存在 `AbstractRedisChannelMessageListener` Bean框架会自动注册 Redis 监听容器。
## 2. 使用 Redis Pub/Sub广播
适用场景: 通知广播、在线会话广播、对可靠性要求不高但强调实时性。
### 2.1 定义消息
```java
import com.njcn.rdms.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
import lombok.Data;
@Data
public class DemoNotifyMessage extends AbstractRedisChannelMessage {
private Long userId;
private String content;
// 可选: 自定义 channel不重写时默认是类名
@Override
public String getChannel() {
return "demo:notify";
}
}
```
### 2.2 定义消费者
```java
import com.njcn.rdms.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class DemoNotifyListener extends AbstractRedisChannelMessageListener<DemoNotifyMessage> {
@Override
public void onMessage(DemoNotifyMessage message) {
log.info("收到广播消息 userId={}, content={}", message.getUserId(), message.getContent());
}
}
```
### 2.3 发送消息
```java
import com.njcn.rdms.framework.mq.redis.core.RedisMQTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DemoNotifyProducer {
private final RedisMQTemplate redisMQTemplate;
public void send(Long userId, String content) {
redisMQTemplate.send(new DemoNotifyMessage()
.setUserId(userId)
.setContent(content));
}
}
```
## 3. 使用 Redis Stream分组消费 + ACK
适用场景: 异步任务、可恢复消费、希望具备重投和清理机制。
### 3.1 定义消息
```java
import com.njcn.rdms.framework.mq.redis.core.stream.AbstractRedisStreamMessage;
import lombok.Data;
@Data
public class DemoTaskMessage extends AbstractRedisStreamMessage {
private Long taskId;
private String bizType;
// 可选: 自定义 stream key不重写时默认是类名
@Override
public String getStreamKey() {
return "demo:task:stream";
}
}
```
### 3.2 定义消费者
```java
import com.njcn.rdms.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class DemoTaskListener extends AbstractRedisStreamMessageListener<DemoTaskMessage> {
@Override
public void onMessage(DemoTaskMessage message) {
log.info("消费任务 taskId={}, bizType={}", message.getTaskId(), message.getBizType());
// 这里抛异常会导致本次消费失败,不会执行 ACK
}
}
```
### 3.3 发送消息
```java
import com.njcn.rdms.framework.mq.redis.core.RedisMQTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DemoTaskProducer {
private final RedisMQTemplate redisMQTemplate;
public RecordId send(Long taskId, String bizType) {
return redisMQTemplate.send(new DemoTaskMessage()
.setTaskId(taskId)
.setBizType(bizType));
}
}
```
## 4. 消息拦截器扩展(可选)
可通过实现 `RedisMessageInterceptor`,对发送/消费前后做统一处理(比如租户透传、审计埋点)。
```java
import com.njcn.rdms.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import com.njcn.rdms.framework.mq.redis.core.message.AbstractRedisMessage;
import org.springframework.stereotype.Component;
@Component
public class DemoRedisMessageInterceptor implements RedisMessageInterceptor {
@Override
public void sendMessageBefore(AbstractRedisMessage message) {
message.addHeader("source", "rdms-system");
}
}
```
## 配置说明
本模块本身几乎没有 `rdms.mq.*` 配置项,主要依赖:
1. `spring.data.redis.*`Redis 连接)
2. `spring.application.name`Stream consumer group 默认值)
另外:
1. Redis Stream 要求 Redis 版本 >= 5.0
2. 只有存在 Stream 监听器时,重投/清理定时任务才会生效
## 与 WebSocket 的关系
`rdms-spring-boot-starter-websocket` 直接复用本模块能力:
1. `sender-type=redis` 时使用 `RedisMQTemplate` + `AbstractRedisChannelMessageListener`
2. `sender-type=rabbitmq/rocketmq/kafka` 时切换到对应中间件实现
你可以参考 websocket 模块作为本模块的落地样板。
## 当前已知注意点
1. `AbstractRedisStreamMessageListener` 有一个 `(streamKey, group)` 构造器,内部把 `messageType` 置空;而消费反序列化仍依赖 `messageType`
建议优先使用无参构造路径(即泛型推断消息类型的默认方式)。
2. Stream 的重投机制本质是“至少一次投递”语义,业务侧应自行保证幂等。
## 适用与不适用
适用:
1. 需要快速接入 Redis 广播/异步任务队列
2. 希望保留基础的失败补偿和消息清理能力
3. 希望通过抽象基类统一消息定义风格
不适用:
1. 对事务一致性、延迟、吞吐有强约束且需要完整 MQ 运维体系
2. 需要复杂顺序语义、死信重试拓扑、跨机房高可用治理