feat(mq-starter-idempotent-redis): mq.idempotent.* 中立配置 + 不按 mq.type 门控的去重 store 自动装配

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 03:12:06 +01:00
parent c2a31c9339
commit f558f6303e
5 changed files with 197 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
package com.njcn.mq.idempotent.redis;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
/**
* 消费去重配置(driver 中立,前缀 {@code mq.idempotent})。
*
* <p>自 mq.redis-stream.idempotent.* 中立化迁移而来,<b>默认值与迁移前完全一致</b>,不改变现有行为。
* 旧键仍向后兼容(见 MqIdempotentRedisAutoConfiguration 的 Binder 回退),但会打弃用告警。
*
* <pre>
* mq:
* idempotent:
* enabled: true # 逃生开关:显式 false 关闭去重 store 装配
* key-prefix: "mq:idem:"
* processing-ttl: 60s # 应大于单条业务处理时延
* success-ttl: 300s # 防重复消费窗口
* fail-ttl: 300s # 允许重新消费前的保留窗口
* </pre>
*/
@Data
@ConfigurationProperties(prefix = "mq.idempotent")
public class MqIdempotentProperties {
/** 逃生开关:显式 false 关闭去重 store 装配。默认开。 */
private boolean enabled = true;
/** 去重键前缀。 */
private String keyPrefix = "mq:idem:";
/** processing 标记 TTL:应大于单条业务处理时延。 */
private Duration processingTtl = Duration.ofSeconds(60);
/** success 标记 TTL:防重复消费窗口。 */
private Duration successTtl = Duration.ofSeconds(300);
/** fail 标记 TTL:允许重新消费前的保留窗口。 */
private Duration failTtl = Duration.ofSeconds(300);
}

View File

@@ -0,0 +1,44 @@
package com.njcn.mq.idempotent.redis;
import com.njcn.mq.autoconfig.MqCoreAutoConfiguration;
import com.njcn.mq.spi.MqIdempotentStore;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 消费去重 store 自动装配(driver 中立)。
*
* <p><b>刻意不按 mq.type 门控</b>,只按「Redis 在场」门控 —— rocketmq / redis-stream / 未来任何
* driver 部署下,只要 classpath 有 spring-data-redis 且上下文有 StringRedisTemplate 就装配。
*
* <p><b>启动安全约束</b>:本装配与 store 构造均不得 eager 连接 / PING Redis(保持 lettuce 懒连接)。
* classpath 有 lettuce 时 Spring Boot 会用 localhost:6379 默认值自动造出 StringRedisTemplate,
* 因此 @ConditionalOnBean 几乎恒真,它不代表 Redis 已正确配置;真正的安全网是懒连接 +
* core 仅在 idempotent=true 时才调用 store(未开去重时 bean 造了也无人调用,零副作用)。
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@AutoConfigureBefore(MqCoreAutoConfiguration.class)
@ConditionalOnClass(StringRedisTemplate.class)
@ConditionalOnProperty(name = "mq.idempotent.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(MqIdempotentProperties.class)
public class MqIdempotentRedisAutoConfiguration {
@Bean
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean(MqIdempotentStore.class)
public MqIdempotentStore mqIdempotentStore(StringRedisTemplate redis, MqIdempotentProperties props) {
return new RedisMqIdempotentStore(redis, props.getKeyPrefix(),
props.getProcessingTtl().getSeconds(), props.getSuccessTtl().getSeconds(),
props.getFailTtl().getSeconds());
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.njcn.mq.idempotent.redis.MqIdempotentRedisAutoConfiguration