定时任务调用外部接口时,超时并不等于失败:服务端可能已经写入,只是响应丢了。直接重试会产生重复记录。下面是一套可复现的最小方案。
架构取舍
采用三层约束:
- 调用方为一次业务意图生成稳定的
idempotency_key,重试时保持不变。 - 接收方在数据库中为该键建立唯一约束,并把业务写入与幂等记录放进同一事务。
- 后续通知通过 outbox 表异步发送,避免数据库提交成功而消息发送失败。
核心表可以从下面的 PostgreSQL 定义开始:
create table idempotency_records (
key text primary key,
status text not null,
response_json jsonb,
created_at timestamptz not null default now()
);
create table outbox_events (
id bigserial primary key,
idempotency_key text not null unique,
event_type text not null,
payload jsonb not null,
sent_at timestamptz
);
请求进入后先尝试插入 idempotency_records。插入成功者执行一次业务写入并生成一条 outbox 事件;遇到唯一键冲突者读取已保存的响应。两条路径都返回同一个业务结果。
可复现验收
用同一个键并发发送 20 次请求,然后检查:
select count(*) from idempotency_records where key = 'demo-001';
select count(*) from outbox_events where idempotency_key = 'demo-001';
两条查询都应返回 1,20 个请求应得到相同的业务对象标识。再换一个新键发送请求,两张表的计数应各增加一条。
边界与代价
幂等键必须绑定请求参数摘要;同一个键携带不同参数时应返回冲突,不能静默复用旧结果。记录还需要设置合理保留期,但清理后再次使用旧键会被视为新请求。这个方案多一次数据库读写和一张 outbox 表,换来的是可审计、可重放且不会重复产生业务副作用的重试机制。