Files
mq-starter/README.md

253 lines
11 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.

# mq-starter
把消息队列封装成一套统一接口的 Spring Boot Starter。业务代码用 `MqTemplate` 发、用 `@MqListener` 收,底层到底走 RocketMQ 还是 Redis Stream交给一行配置决定切换的时候业务代码一行都不用动。
思路和 JDBC 差不多:你面向 `Connection` / `Statement` 写代码,换 MySQL 还是 Oracle 只是改驱动和连接串;这里换 MQ 只是改 `mq.type`
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
[![JDK](https://img.shields.io/badge/JDK-1.8-orange.svg)](https://www.oracle.com/java/)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.3.12-brightgreen.svg)](https://spring.io/projects/spring-boot)
```yaml
mq:
type: redis-stream # 换成 rocketmq 也行,业务代码不动
```
```java
@Resource MqTemplate mq;
mq.send("LN_Topic", payload);
@MqListener(topic = "LN_Topic", group = "ln_consumer")
public void onData(FrontData msg) {
// msg 已经反序列化好了,这里只写业务
}
```
## 为什么写它
之前项目里跟 MQ 打交道producer / consumer 的注册、ack、序列化、失败重试、去重这些样板代码散落得到处都是真要把 RocketMQ 换成别的,消费层基本得重写一遍。
mq-starter 想做的就是把这些脏活收进 starter业务侧只保留「发什么」和「收什么」剩下的连接管理、线程模型、确认机制都不用管。顺手把消费侧几个常见需求去重、批量、失败落库也做成了注解开关。
## 快速开始
### 引入
业务方只需要引一个聚合坐标,两套 driver 都打在里面,运行时按 `mq.type` 二选一装配:
```xml
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mq-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
目前还没发到中央仓库,先在项目根目录 `mvn install` 装到本地仓库用。
### 配置
用 Redis Stream
```yaml
mq:
type: redis-stream
spring:
redis:
host: localhost
port: 6379
```
用 RocketMQ
```yaml
mq:
type: rocketmq
rocketmq:
name-server: localhost:9876
producer:
group: my-producer-group
```
`mq.type` 是唯一的装配开关。没配它,`MqTemplate``@MqListener` 都不会生效。
### 发送
```java
@Resource
private MqTemplate mq;
mq.send("LN_Topic", frontData); // topic + 消息体
mq.send("File_Topic", nodeId, fileData); // 带路由 key映射成 tag
```
消息体传任意对象就行driver 负责序列化。每条消息会自动生成一个雪花 ID 当 key去重时会用到。
### 消费
方法上打个注解就行方法参数写你期望的类型starter 按类型反序列化:
```java
@Component
public class FrontDataConsumer {
@MqListener(topic = "LN_Topic", group = "ln_consumer")
public void onFrontData(FrontData msg) {
// ...
}
// 想拿 key / tag / timestamp / headers 这些元数据,就再加一个 MqContext 参数
@MqListener(topic = "File_Topic", group = "file_consumer")
public void onFile(FileData msg, MqContext ctx) {
String key = ctx.getKey();
}
}
```
方法签名只认这两种:`(payload)``(payload, MqContext)`
## 进阶用法
去重、批量、失败落库都是 `@MqListener` 上的属性,业务方法本身不用改:
```java
// 去重:重复消息不会再进业务(去重状态存 Redis两个 driver 都支持,见下方说明)
@MqListener(topic = "LN_Topic", group = "g", idempotent = true)
public void onData(FrontData msg) { }
// 批量:攒够 batchSize 条一次给你,参数要用 List
@MqListener(topic = "LN_Topic", group = "g", batchSize = 50)
public void onBatch(List<FrontData> msgs) { }
// 失败落库:消费失败的消息交给你自己的 handler 落库
@MqListener(topic = "LN_Topic", group = "g", errorHandler = "myErrorHandler")
public void onData2(FrontData msg) { }
```
落库 handler 自己实现一个 bean
```java
@Component("myErrorHandler")
public class MyErrorHandler implements MqConsumeErrorHandler {
@Override
public void onError(MqMessage message, MqErrorIdentity identity, Exception ex) {
// identity 区分是单次失败(SINGLE)还是重试耗尽(RETRY_EXHAUSTED)
// 把 message 存到异常表,后面人工或定时补偿
}
}
```
## 两个 driver 不太一样
两套实现的能力大头已经对齐:去重、失败落库两端都接上了。剩下的差异主要在死信模型和批量语义:
| 能力 | Redis Stream | RocketMQ |
|---|:---:|:---:|
| 收发 / 并发消费 | ✅ | ✅ |
| 去重 `idempotent` | ✅(需 Redis | ✅(需 Redis |
| 失败落库 `RETRY_EXHAUSTED` | ✅ | ✅ 落库并 ACK |
| 失败落库 `SINGLE` | ⚠️ 单条:落库+重投;批量:失败被吞(两端一致) | ⚠️ 单条:落库+重投;批量:失败被吞(两端一致) |
| 批量 `batchSize` | ⚠️ core 内存攒批driver 逐条喂);无 idle flush低频停滞/重启丢未满批(两端一致) | ⚠️ core 内存攒批driver 逐条喂);无 idle flush低频停滞/重启丢未满批(两端一致) |
| 重试 | ✅ PEL reclaim | ✅ 原生重投 |
| 死信队列 | ❌ 只落库 | ✅ 原生 `%DLQ%` |
| 顺序消息 | ❌ | ❌ 用的 Concurrently |
| 事务 / 延迟消息 | ❌ | ❌ 没暴露 |
| 序列化 | fastjson | fastjson |
去重的状态存在 Redis`processing / success / fail` 三态 + TTL由独立模块 `mq-starter-idempotent-redis` 提供,**不看 `mq.type`**——所以 RocketMQ 部署只要能连上 Redis标准 `spring.redis.*` 配置),`idempotent = true` 一样生效。引聚合坐标的话这个模块已经打在里面;单独只引 `mq-starter-rocketmq` 的话需要自己再加一个 `mq-starter-idempotent-redis` 依赖。没开去重时它完全不碰 Redis懒连接不会因为 Redis 没配好而影响启动。
## 架构
```
业务代码(只见 MqTemplate / @MqListener
┌──────────────────────┴──────────────────────┐
│ mq-starter-core │
│ MqTemplate · @MqListener 扫描 · MqMessage │
│ 消费链:去重 → 攒批 → 消费 → 落库/重试 │
│ SPIMqDriver / MqSubscription / ... │
└──────────────┬──────────────────┬────────────┘
@ConditionalOnProperty(mq.type) 二选一
┌──────────────┴─────┐ ┌─────────┴───────────┐
│ redis-stream driver │ │ rocketmq driver │
└─────────────────────┘ └─────────────────────┘
```
五个模块:
| 模块 | 干什么 |
|---|---|
| `mq-starter-core` | 门面 `MqTemplate``@MqListener` 扫描注册、中立信封 `MqMessage`、消费处理链、几个 SPI还有基于 `SmartLifecycle` 的启停 |
| `mq-starter-idempotent-redis` | 消费去重的 Redis 存储driver 中立,不看 `mq.type`,有 Redis 就装配;配置前缀 `mq.idempotent` |
| `mq-starter-redis-stream` | Redis Stream 实现XADD / 消费组 XREADGROUP / XACK / XPENDING+XCLAIM reclaim去重经依赖 idempotent-redis 获得 |
| `mq-starter-rocketmq` | RocketMQ 实现,基于 rocketmq-spring 编程式注册 |
| `mq-spring-boot-starter` | 聚合 jar业务方引这一个就够恒含 idempotent-redis |
core 是中立的,编排逻辑不依赖 Spring 和 Redis可以直接单测。想看更细的图文消费链、SPI 契约、配置速查、FAQ 都在里面),打开 [`docs/mq-starter-全景手册.html`](./docs/mq-starter-全景手册.html)。
## 配置项
`@MqListener` 的属性:
| 属性 | 默认 | 说明 |
|---|---|---|
| `topic` / `group` | 必填 | 主题 / 消费组 |
| `concurrency` | 1 | 并发消费线程数,实际由 driver 兑现 |
| `batchSize` | 1 | 攒批大小,大于 1 要用 `List` 参数 |
| `maxRetry` | 3 | 最大重试次数 |
| `tag` | `*` | tag 过滤 |
| `idempotent` | false | 是否去重 |
| `errorHandler` | `""` | 失败落库的 bean 名 |
去重参数(前缀 `mq.idempotent`driver 中立,都有默认值):
| 配置 | 默认 | 说明 |
|---|---|---|
| `enabled` | true | 逃生开关,显式 false 关闭去重 store 装配 |
| `processing-ttl` | 60s | 「处理中」标记存多久建议大于单条处理耗时rocketmq 下还建议小于业务重试节奏的累计时长,否则并发中的消息可能反复重投直至进原生 `%DLQ%` |
| `success-ttl` | 300s | 「已完成」标记存多久(去重窗口) |
| `fail-ttl` | 300s | 失败标记保留多久 |
| `key-prefix` | `mq:idem:` | 去重键前缀 |
> 旧键 `mq.redis-stream.idempotent.*` 仍向后兼容(会打一条弃用告警),中立键和旧键同时配置时以 `mq.idempotent.*` 为准。建议尽快迁移。
Redis Stream 这边的参数(前缀 `mq.redis-stream`
| 配置 | 默认 | 说明 |
|---|---|---|
| `reclaim-min-idle` | 30s | 消息投出去多久没 ack 才 reclaim 重投,要大于单条处理耗时(且建议大于 processing-ttl |
RocketMQ 这边直接复用 rocketmq-spring 原生的 `rocketmq.*` 配置。
## 已知限制 / TODO
按优先级列,详细的在 [`docs/mq-starter-改进优化清单.md`](./docs/mq-starter-改进优化清单.md)。
P0放量到生产前想补的
- [x] 失败落库、去重、批量消费core 已做)
- [x] 给 rocketmq driver 补上去重和 `RETRY_EXHAUSTED` 落库,跟 redis-stream 对齐(去重 store 已抽成 driver 中立的 `mq-starter-idempotent-redis`
- [ ] 批量消费加 idle flush现在低流量下攒不满会卡着进程重启还会丢内存里没攒满的那批
- [ ] 顺序消息
- [ ] 事务消息
P1延迟消息、死信队列统一抽象、消息轨迹 / 指标、序列化可插拔(两端都是 fastjson但硬编码换不掉、广播消费。
P2并发度语义统一、headers 完整透传、`mq.type` 错配时 fail-fast、监控告警。
## 构建
```bash
git clone https://github.com/EdmundNightmare/mq-starter.git
cd mq-starter
mvn clean install
```
需要 JDK 1.8+ 和 Maven 3.6+。单元测试 `mvn test` 不依赖外部中间件;带真实 Redis 的集成测试(`*IT`)在 `mvn verify` 阶段跑。
有想法欢迎提 issue 或 PR优先认领上面 P0 的几项,提交前跑一下 `mvn verify`,改动尽量带上测试。
## License
[Apache License 2.0](./LICENSE)Copyright 2026 EdmundNightmare。