feat(mq-starter-idempotent-redis): 新建 driver 中立去重模块,迁入 RedisMqIdempotentStore 及其测试

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 10:04:10 +08:00
parent f4793b10c0
commit c2a31c9339
5 changed files with 172 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
package com.njcn.mq.idempotent.redis;
import com.njcn.mq.spi.MqIdempotentStore;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.Duration;
/**
* 基于 Redis 的去重默认实现。状态机 processing / success / fail各带 TTL。
*
* <p>对齐 legacy filter+consumeSuccess+saveException 的语义,但修正了 legacy「读裸 key、写带前缀 key」
* 导致去重失效的缺陷:本实现读写统一使用 {@code prefix + key}。
*/
public class RedisMqIdempotentStore implements MqIdempotentStore {
private static final String PROCESSING = "processing";
private static final String SUCCESS = "success";
private static final String FAIL = "fail";
private final StringRedisTemplate redis;
private final String prefix;
private final long processingTtl;
private final long successTtl;
private final long failTtl;
public RedisMqIdempotentStore(StringRedisTemplate redis, String prefix,
long processingTtlSeconds, long successTtlSeconds, long failTtlSeconds) {
this.redis = redis;
this.prefix = prefix;
this.processingTtl = processingTtlSeconds;
this.successTtl = successTtlSeconds;
this.failTtl = failTtlSeconds;
}
@Override
public boolean tryAcquire(String key) {
String k = prefix + key;
// setIfAbsent 原子占用key 不存在时写 processing 并放行。
Boolean acquired = redis.opsForValue().setIfAbsent(k, PROCESSING, Duration.ofSeconds(processingTtl));
if (Boolean.TRUE.equals(acquired)) {
return true;
}
// key 已存在fail上次失败允许重新消费processing / success 一律挡回。
// 注意fail→reopen 的 get+set 非原子,跨实例对「共享同一 key 的不同消息」可能都放行;
// 同一条消息由 stream PEL/claim 序列化,故仅影响不同消息共享 key 的边缘场景。
if (FAIL.equals(redis.opsForValue().get(k))) {
redis.opsForValue().set(k, PROCESSING, Duration.ofSeconds(processingTtl));
return true;
}
return false;
}
@Override
public void markSuccess(String key) {
redis.opsForValue().set(prefix + key, SUCCESS, Duration.ofSeconds(successTtl));
}
@Override
public void markFail(String key) {
redis.opsForValue().set(prefix + key, FAIL, Duration.ofSeconds(failTtl));
}
@Override
public boolean isCompleted(String key) {
// 仅 success 视为"已完成"(driver 可 ACK)。processing(并发处理中/崩溃残留)与
// 无记录(标记已过期)一律 false使 driver 保留消息、待重投,避免静默丢消息。
return SUCCESS.equals(redis.opsForValue().get(prefix + key));
}
}