Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
redis-stream-springboot-starter
基于 Redis Streams 的消息收发 SDK,对标 rocket-mq-springboot-starter,零业务依赖、开箱即用。
引入依赖即自动装配:配好 Redis 连接,注入 RedisStreamEnhanceTemplate 即可发送;注册一个 EnhanceStreamConsumerHandler Bean 即可消费。框架负责消费组创建、阻塞拉取、ACK、崩溃恢复(autoclaim)、死信淘汰、Stream 定时裁剪。
- Spring Boot
2.3.12.RELEASE/ JDK1.8 - 底层
spring-data-redis 2.3.x+ Lettuce - 消息体可选 gzip 压缩;XADD 字段契约固定为
key/tag/enc/body/ts(可与 C++ 等异构端互通)
一、引入依赖
<dependency>
<groupId>com.njcn</groupId>
<artifactId>redis-stream-springboot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
制品发布在内网 Nexus,若拉不到依赖请确认 settings.xml / pom.xml 配置了仓库:
<repository>
<id>nexus-snapshots</id>
<url>http://192.168.1.22:8001/nexus/content/repositories/snapshots/</url>
</repository>
无需
@EnableXxx注解。starter 通过spring.factories自动装配,且在RedisAutoConfiguration之后生效(确保StringRedisTemplate已就绪)。
二、最小配置
连接信息走标准 Spring Redis 配置,starter 自身配置前缀为 redis-stream:
spring:
redis:
host: 127.0.0.1
port: 6379
# password: ...
# database: 0
redis-stream:
# 以下均为可选,默认值见「配置项」一节
read-count: 10
block-ms: 2000
max-retry: 3
三、定义消息
业务消息继承 StreamBaseMessage(starter 自带,不依赖任何业务 api):
import com.njcn.middle.stream.domain.StreamBaseMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class OrderMessage extends StreamBaseMessage {
private Long orderId;
private String status;
}
基类已内置以下字段,业务无需自己维护:
| 字段 | 默认值 | 说明 |
|---|---|---|
key |
雪花 id | 消息业务 key,全局单例 Snowflake 生成,保证唯一 |
tag |
"" |
下行路由 / 过滤标识 |
source |
"" |
消息来源标识 |
sendTime |
now() |
发送时间 |
retryTimes |
0 |
预留字段(重试计数实际由 Redis PEL 维护,见下文) |
四、发送消息
注入 RedisStreamEnhanceTemplate,调用 send(topic, message, compress):
import com.njcn.middle.stream.template.RedisStreamEnhanceTemplate;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.stereotype.Service;
@Service
public class OrderProducer {
private final RedisStreamEnhanceTemplate streamTemplate;
public OrderProducer(RedisStreamEnhanceTemplate streamTemplate) {
this.streamTemplate = streamTemplate;
}
public void sendOrder(Long orderId) {
OrderMessage msg = new OrderMessage();
msg.setOrderId(orderId);
msg.setStatus("CREATED");
// 需要按 tag 路由时,发送前先 setTag(...)
msg.setTag("node-1");
// 第三参 compress=true 时对 body 做 gzip+base64;false 为明文 json
RecordId id = streamTemplate.send("order-topic", msg, false);
}
}
要点:
topic会经StreamKeyBuilder转为实际 stream 名(开启环境隔离时追加_env后缀)。key/tag从 message 对象取,不单独传参。需要自定义 tag 时,发送前message.setTag(...)。- 返回
RecordId即 XADD 生成的消息 id。
五、消费消息
继承 EnhanceStreamConsumerHandler<T> 并注册为 Spring Bean,框架会为每个 handler 起一条消费线程:
import com.njcn.middle.stream.handler.EnhanceStreamConsumerHandler;
import org.springframework.stereotype.Component;
@Component
public class OrderConsumer extends EnhanceStreamConsumerHandler<OrderMessage> {
@Override
public String topic() {
return "order-topic"; // 与发送方一致
}
@Override
public String group() {
return "order-consumer-group"; // 消费组名,启动时自动 XGROUP CREATE
}
@Override
public Class<OrderMessage> messageType() {
return OrderMessage.class; // 用于反序列化
}
@Override
public void handleMessage(OrderMessage message) throws Exception {
// 业务处理。抛异常时按 isRetry() 决定 RETRY 或落异常
System.out.println("consume order = " + message.getOrderId());
}
@Override
public boolean isRetry() {
return true; // 失败是否重试(留 PEL 由 autoclaim 重投)
}
@Override
public boolean throwException() {
return false; // Redis 模式下为 no-op,固定返回 false 即可
}
}
必须实现的方法
| 方法 | 说明 |
|---|---|
topic() |
监听的 topic |
group() |
消费组名(自动幂等建组) |
messageType() |
消息类型,用于反序列化 |
handleMessage(T) |
业务处理逻辑 |
isRetry() |
处理失败是否重试 |
throwException() |
Redis 模式 no-op,返回 false 即可(仅为与 rocket starter 共用接口而保留) |
可选覆写的钩子
| 方法 | 默认 | 说明 |
|---|---|---|
filter(T) |
false |
返回 true 则直接 ACK 丢弃,不进入 handleMessage |
consumeSuccess(T) |
空 | 消费成功回调 |
saveExceptionMsgLog(T message, body, e) |
空 | 异常落库 / 告警回调(毒消息 / 不重试失败 / 死信触发);message 可能为 null,实现需判空 |
getMaxRetryTimes() |
沿用全局 | 覆写为正整数即只对本 handler 单独设定最大投递次数,优先级高于全局 redis-stream.max-retry |
💡 不覆写
getMaxRetryTimes()时,该 handler 沿用全局配置redis-stream.max-retry;需要某个 handler 单独控制重试次数时,覆写它返回一个正整数即可(如重投较多的关键 topic 返回10)。⚠️
saveExceptionMsgLog的message入参:不重试失败与死信(autoclaim 超max-retry)场景下为反序列化后的业务对象;仅毒消息(body本身无法解析)场景下为null(此时只能用原始body)。回调实现务必对message == null做判空,避免 NPE。
六、消费语义:成功 / 重试 / 毒消息 / 死信
一条消息进入 handleMessage 后的处理结果:
解码(enc,body) → 反序列化 → filter → handleMessage → consumeSuccess → XACK
| 场景 | 结果 |
|---|---|
| 处理成功 | XACK 确认 |
filter() 返回 true |
XACK 丢弃,不处理 |
| 解码 / 反序列化失败(毒消息) | saveExceptionMsgLog + XACK,不卡队列 |
handleMessage 抛异常 且 isRetry()=true |
RETRY:不确认,留 PEL |
handleMessage 抛异常 且 isRetry()=false |
saveExceptionMsgLog + XACK |
重试与死信(崩溃恢复) 由后台 StreamAutoClaimer 兜底:
- RETRY 的消息留在 PEL(Pending Entries List)中未确认。
- autoclaim 周期性扫描 PEL,对 idle 超过
autoclaim.min-idle-ms的消息执行XCLAIM认领并重投。 - 每次
XCLAIM使投递计数 +1;当投递次数达到max-retry(含首投)仍失败 → 触发saveExceptionMsgLog(可拿到反序列化后的业务消息,异常信息含max retry exceeded)+ 强制XACK,作为死信淘汰,避免无限滞留。
即:
max-retry=3⇒ 最多投递 3 次(首投 1 次 + autoclaim 重投 2 次),第 3 次仍失败即落死信。 单个 handler 可覆写getMaxRetryTimes()返回正整数,对该 handler 单独设定最大投递次数,优先级高于全局redis-stream.max-retry。 实例宕机导致的 PEL 残留消息,也会被其它存活实例的 autoclaim 接管重投。
七、配置项
| 配置项 | 默认值 | 说明 |
|---|---|---|
redis-stream.env-isolation |
false |
是否按环境隔离 stream 名 |
redis-stream.env |
"" |
环境标识,隔离开启且非空时作为 stream 名后缀(topic_env) |
redis-stream.consumer-name |
"" |
消费者名;为空时自动生成唯一名(见下方注意事项) |
redis-stream.read-count |
10 |
单次 XREADGROUP 拉取条数 |
redis-stream.block-ms |
2000 |
XREADGROUP 阻塞毫秒 |
redis-stream.max-retry |
3 |
全局最大投递次数(含首投),超限落死信;可被 handler 的 getMaxRetryTimes() 单独覆写 |
redis-stream.autoclaim.min-idle-ms |
60000 |
消息 idle 多久后可被认领重投 |
redis-stream.autoclaim.interval-ms |
30000 |
autoclaim 扫描周期 |
redis-stream.trim.interval-ms |
60000 |
XTRIM 执行周期 |
redis-stream.trim.maxlen |
100000 |
近似保留的最大 stream 长度(MAXLEN ~) |
八、注意事项
- 多实例部署不要写死
consumer-name。留空时框架会为每个实例生成唯一消费者名(consumer-xxx/claimer-xxx),符合消费组负载均衡模型。若多个实例配置了相同的固定consumer-name,它们会被视为同一个消费者而互相争抢,破坏负载均衡 ——consumer-name仅建议用于单实例或本地调试。 - 环境隔离:多环境(dev/test/prod)共用一套 Redis 时,开启
env-isolation并设置env,各环境 stream 名互不串扰。收发双方必须配置一致,否则 topic 对不上。 - 压缩契约:仅
body字段可压缩。compress=true⇒enc=gzip、body=base64(gzip(json));否则enc=plain、body为明文 json。消费侧仅按enc解码,与 topic 无关,可与 C++ 等异构端互通。 - Stream 裁剪:框架按
trim.maxlen近似裁剪 stream 防止无界增长。请确保maxlen足够大,避免重试中的消息在达到max-retry前被裁掉(被裁掉的 PEL 消息会被静默强制 ACK)。 - 无 handler 时容器空转:未注册任何
EnhanceStreamConsumerHandlerBean 时,监听/认领/裁剪线程均不启动,仅RedisStreamEnhanceTemplate可用于纯发送场景。
九、本地验证(可选)
集成测试需要本地 Redis;不可达时整类自动 skip(不影响 mvn test 全绿):
docker run -d -p 6379:6379 redis:6.2
mvn test