为每个请求生成一个唯一的Token,并在服务端进行校验,一旦处理了对应的请求,就丢弃该Token,避免重复处理。具体步骤:
1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
核心逻辑:
// 服务端接口,接收请求并处理token
void do(String token) {
if (Redis.exists(token)) {
// 删除token,确保不会重复处理
Redis.del(token);
// 执行具体的业务操作
doSometing();
} else {
log.info(token);
}
}
注意:最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。可以在 redis 使用 lua 脚本完成这个操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
6. 状态机
使用状态机是判断业务流程,确保操作只执行一次。
状态机设计:
订单创建:订单初始化,状态为 PENDING(待支付)。
支付操作:当订单状态为 PENDING 时,允许执行支付操作,支付成功后状态变为 PAID(已支付)。
重复支付检查:如果再次尝试支付一个已经是 PAID 状态的订单,状态机将拒绝该操作,保持订单状态不变。
实现案例
public enum OrderStatus {
PENDING, PAID, CANCELLED
}
public class Order {
private OrderStatus status; // 订单当前状态
// 其他订单属性...
public Order() {
this.status = OrderStatus.PENDING; // 初始化状态为待支付
}
// 执行支付操作
public synchronized void pay() {
if (this.status == OrderStatus.PENDING) {
// 执行支付逻辑,如减少库存、扣款等
this.status = OrderStatus.PAID; // 状态转变为已支付
} else {
// 如果订单不是在待支付状态,抛出异常或记录日志
throw new IllegalStateException("Order can only be paid when status is PENDING");
}
}
// 其他业务逻辑...
}
幂等性保证:
支付操作 pay 在订单状态不是 PENDING 时不会被执行,从而保证了幂等性。
如果有重复的支付请求,由于状态机的保护,第二次及后续的支付请求将不会改变订单状态,因此不会执行重复的支付逻辑。
7. 去重表
记录已经处理过的请求标识,对于重复的请求直接返回结果,而不再次执行业务逻辑。
1、去重表结构设计
表字段至少包括:
请求标识符:唯一标识一次请求。
创建时间:记录请求的时间戳。
处理状态:标识请求是否已处理,以及处理的结果。
2、设置过期策略
为了防止去重表无限增长,表中的记录可以设置过期时间。使用定时任务定期清理旧的请求记录。
实现案例:
1、检查去重表
在执行业务逻辑之前,检查去重表确定该请求是否已经被处理过。
boolean isDuplicate = checkDuplicateInDatabase(requestId);
2、处理请求
if (isDuplicate) {
// 返回之前的结果或拒绝处理
return previousResult;
} else {
// 执行业务逻辑
doSomthing();
// 记录去重表
saveRecord(requestId);
// 返回新的结果
return newResult;
}
注意事项:
数据一致性:确保去重表的更新与业务逻辑的执行保持一致性,避免出现数据不一致的情况。
8. 全局请求唯一ID
调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。
可以使用 nginx 设置每一个请求的唯一 id;
proxy_set_header X-Request-Id $request_id;