本文目录导读:

这是一个非常经典且具有挑战性的分布式系统设计问题。超时重试本身是保证系统可靠性的手段,但幂等是保证重试后数据一致性不出错的核心防线。
幂等设计无论客户端发起多少次重试,服务端处理一次和处理多次的结果完全一致。
以下是针对“超时重试”场景下的幂等设计方案,从策略到落地代码思路:
核心思路:唯一请求标识 + 状态机 + 结果缓存
解决幂等的关键在于区分“新请求”和“重试请求”,并确保对同一业务数据的修改只生效一次。
全局唯一请求ID(核心基石)
原理:客户端在首次发起请求时,生成一个全局唯一的 requestId(如 UUID、雪花算法),服务端在处理请求时,以 requestId 为 key 记录处理状态。
- 客户端:每次请求(无论成功失败)都携带同一个
requestId。 - 服务端:在处理前,先查询
requestId是否存在。
代码伪代码(Java + Redis):
public Response processPayment(PaymentRequest request) {
String requestId = request.getRequestId();
// 1. 查重:检查该 requestId 是否已处理
// Redis setnx 是原子操作,key 不存在则设置成功,返回 1;已存在则返回 0
boolean isNewRequest = redisTemplate.opsForValue()
.setIfAbsent("PAYMENT:" + requestId, "PROCESSING", 30, TimeUnit.MINUTES);
if (!isNewRequest) {
// 2. 如果已存在,说明是重试请求
// 释放锁(如果是分布式锁场景则需注意)
// 直接返回之前处理的结果(需要缓存结果)
return getPreviousResult(requestId);
}
try {
// 3. 执行业务逻辑:扣款、入账等
// ... 数据库操作 ...
// 4. 成功后,更新状态为 SUCCESS,并缓存结果
redisTemplate.opsForValue().set("PAYMENT:" + requestId, "SUCCESS");
return Response.success();
} catch (Exception e) {
// 5. 失败时,删除标识,允许客户端重试(但需注意防并发)
redisTemplate.delete("PAYMENT:" + requestId);
throw e; // 让客户端感知失败
}
}
注意:setIfAbsent 带过期时间的做法,可以防止死锁(即请求永远不被清理)。
数据库唯一约束 + 业务键
如果不想依赖 Redis,可以利用数据库本身的唯一索引或主键约束。
- 场景:创建订单、插入流水记录等。
- 做法:在业务表中,将
requestId或业务唯一键(如order_sn + product_id)设为 唯一索引。 - 效果:第一次插入成功,第二次重试插入时,数据库会报
DuplicateKeyException,捕获该异常后,直接返回第一次的成功结果即可。
代码示例:
@Transactional
public Response createOrder(OrderDTO order) {
OrderEntity entity = new OrderEntity();
entity.setRequestId(order.getRequestId()); // 唯一索引字段
// ... 其他字段
try {
orderDao.insert(entity);
return Response.success(entity);
} catch (DuplicateKeyException e) {
// 重试请求,查询已有数据并返回
OrderEntity existed = orderDao.findByRequestId(order.getRequestId());
return Response.success(existed);
}
}
乐观锁(适用于更新操作)
对于更新操作(如扣减库存、修改余额),不能简单使用防重表,因为“状态”会变。
- 场景:支付成功后更新订单状态
from PAYING to PAID。 - 做法:在 SQL 中加入
version或status条件判断。
SQL 示例:
-- 幂等更新:只有当前状态为 PAYING 时才更新成 PAID UPDATE orders SET status = 'PAID', version = version + 1 WHERE order_id = ? AND status = 'PAYING';
如果返回的受影响行数为 0,说明已经是 PAID 状态,直接返回成功即可(幂等已保证)。
状态机(Strict State Machine)
与乐观锁类似,但更加严格,业务数据需要有明确的状态流转路径。
- 原则:只允许
A -> B,不允许B -> B或A -> C。 - 使用:每次重试时,判断当前状态是否已经到达目标状态,如果是,则直接返回成功。
| 当前状态 | 期望操作 | 结果 |
|---|---|---|
| INIT | 支付 | 允许,变成 PAID |
| PAID | 支付(重试) | 不允许,直接返回成功 |
| REFUNDING | 支付 | 不允许,返回错误或忽略 |
高级场景与设计要点
超时重试的“等幂”陷阱
- 问题:客户端发了请求,服务端处理成功,但响应超时,客户端重试。
- 如果服务端重入了业务逻辑(比如两次扣款),就出事了。
- 关键:服务端必须先做幂等检查,再执行逻辑。
- 问题:重试请求到达时,第一次请求还在处理中。
- 解决:使用分布式锁 + 防重表,如果第一次正在处理,第二次请求应等待或快速失败,而不是重复处理。
接口类型的幂等处理差异
| 接口类型 | 天然幂等 | 设计建议 |
|---|---|---|
| 查询 | 是 | 无需特殊处理 |
| 删除 | 大部分是 | 幂等删除(delete by id,多次执行结果一样) |
| 更新 | 否 | 乐观锁、状态机防重 |
| 创建 | 否 | 唯一约束、防重表 |
外部服务(如银行、第三方支付)的幂等
- 依赖:上游系统通常会要求传递
request_no。 - 方案:与第三方约定好幂等键(如订单号),由第三方保证对同一幂等键的去重。
- 注意:不可控的外部服务(如微信支付回调)也可能超时重试,因此接收方(例如你的系统)也要做幂等。
一套完整的幂等设计模板
graph LR
A[客户端] --> B(生成唯一requestId);
B --> C{发起请求};
C --> D[网关/API入口];
D --> E{检查幂等标识};
E -- 新请求 --> F[执行业务逻辑];
E -- 重复请求 --> G[返回缓存结果];
F --> H[更新幂等标识状态];
H -- 成功 --> I[返回结果];
H -- 失败 --> J[清理幂等标识];
J --> C;
黄金定律:
- 先检查,后执行:在业务逻辑入口处,以
requestId作为锁或判断依据。 - 结果缓存:如果请求已被处理,直接返回该
requestId对应的成功结果(避免重试时查不到结果而报错)。 - 防重表 + 分布式锁:在并发情况下,保证只有第一个请求能拿到锁并执行业务。
- 乐观锁/状态机:对于更新操作,利用数据库的版本号或状态字段实现原子性更新。
只要遵循“唯一ID + 前置检查 + 结果缓存”这个组合,超时重试的幂等设计就能非常稳固。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播