2、重试策略基础:重试的幂等性保障——为什么重试必须是安全的?
聊重试之前,我先问你一个问题:一个操作被重复执行两次,结果和只执行一次完全一样吗?
如果答案是否定的,那重试就是个定时炸弹。我见过太多线上事故,就是因为重试没有考虑幂等性——订单重复扣款、库存多扣、短信狂发……说白了,重试本身不产生新问题,但它会把已有的问题放大十倍。
2.1 什么是幂等性?
幂等性(Idempotency)这个词听着挺学术,其实意思很简单:同一个操作,无论执行一次还是一百次,对系统产生的影响是一样的。
举个例子:
- 查询操作:查用户信息,查一万次结果都一样——天然幂等。
- 删除操作:删一条不存在的记录,返回结果可能不同,但系统状态不变——也算幂等。
- 扣款操作:扣用户 100 元,执行两次就扣 200 元——不幂等,这就是重试的雷区。
我习惯把幂等性分成两个层面来看:
| 层面 | 含义 | 典型场景 |
|---|---|---|
| 读幂等 | 多次读取结果一致 | GET 请求、查询数据库 |
| 写幂等 | 多次写入最终状态一致 | PUT 更新、幂等扣款、去重插入 |
重试主要关心的是写幂等。因为读操作重试不会破坏数据,但写操作重试一旦缺乏保障,后果很严重。
2.2 为什么重试必须考虑幂等性?
你想想看,重试的本质是什么?是把同一个请求再发一次。如果下游服务已经成功处理了第一次请求,只是返回结果时网络断了,那第二次请求就会导致重复执行。
我在项目中遇到过这样一个案例:
- 一个支付服务,调用银行接口扣款。
- 第一次调用超时了,客户端触发重试。
- 实际上银行已经扣款成功,只是响应没回来。
- 重试又扣了一次——用户被扣了两笔钱。
这就是典型的非幂等重试事故。用户投诉、客服爆炸、运维背锅……嗯,那滋味不好受。
2.3 幂等性的实现方式
实现幂等性,说白了就是让系统能识别出「这个请求我已经处理过了」。常用的方法有下面几种:
2.3.1 全局唯一请求 ID(Idempotency Key)
这是最通用的方案。每次请求带一个全局唯一的 ID,服务端用这个 ID 做去重。
// 客户端生成请求 ID
String requestId = UUID.randomUUID().toString();
// 服务端处理逻辑
public Response processPayment(Request req) {
// 1. 检查请求 ID 是否已处理
if (idempotencyStore.exists(req.getRequestId())) {
// 直接返回上一次的结果
return idempotencyStore.getResult(req.getRequestId());
}
// 2. 执行实际业务逻辑
Result result = doPayment(req);
// 3. 记录请求 ID 和结果
idempotencyStore.save(req.getRequestId(), result);
return result;
}
这里有个关键点:检查和处理必须是原子操作。如果两个重试请求同时到达,都检查到「不存在」,就会都去执行业务逻辑。所以一般会用 Redis 的 SET NX 或者数据库的唯一索引来做互斥。
2.3.2 数据库唯一约束
对于数据库写入操作,利用唯一索引天然防重。
-- 创建唯一索引
CREATE UNIQUE INDEX idx_order_no ON orders(order_no);
-- 插入时捕获重复键异常
try {
INSERT INTO orders(order_no, amount, status) VALUES('ORD2024001', 100, 'PAID');
} catch (DuplicateKeyException e) {
// 订单已存在,直接返回成功
return getOrder('ORD2024001');
}
这种方式简单粗暴,但只适用于数据库写入场景。而且要注意:唯一索引的字段必须能唯一标识一次业务操作,比如订单号、流水号。
2.3.3 状态机与版本号
对于有状态流转的业务(比如订单状态:待支付 → 已支付 → 已发货),可以用版本号或状态机来保证幂等。
// 乐观锁更新
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE order_id = 'ORD2024001' AND version = 1;
// 如果影响行数为 0,说明已经被更新过了
if (affectedRows == 0) {
// 幂等处理:直接返回成功
return success();
}
这种方式的好处是不需要额外的存储,利用业务数据本身的版本信息就能防重。但前提是业务表必须有版本号字段,而且更新逻辑要严格按版本号来。
2.4 幂等性设计的常见误区
我见过不少团队在幂等性上踩坑,总结几个典型的:
- 以为查询操作不需要幂等——查询确实天然幂等,但如果查询结果被缓存了,重试可能导致缓存穿透,还是要注意。
- 幂等 key 生成太随意——有人用时间戳做幂等 key,结果同一毫秒内的重试生成了不同的 key,完全没起到去重作用。
- 忽略幂等存储的高可用——幂等存储本身如果挂了,所有重试都会变成非幂等请求。我建议幂等存储至少做主从或集群部署。
- 幂等 key 的过期时间太短——重试窗口还没结束,key 就过期了,导致重复处理。
2.5 本章知识体系图
下面这张图梳理了幂等性在重试策略中的核心位置和实现路径:
2.6 小结
幂等性是重试策略的基石。没有它,重试就不是在提升稳定性,而是在制造混乱。
我个人习惯在设计任何接口时,先问自己一个问题:这个接口被重复调用会出问题吗?如果会,那就必须加上幂等保障。不管是请求 ID、唯一约束还是版本号,选一种适合场景的就行。关键是——别等到出事了再补。