1.什么是接口幂等性?
幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。
比如下面这些情况,如果没有实现接口幂等性会有很严重的后果:支付接口,重复支付会导致多次扣钱 ;订单接口,同一个订单可能会多次创建。
2.为什么会产生接口幂等性问题?
那么,什么情况下,会产生接口幂等性的问题呢?
- 网络波动, 可能会引起重复请求
- 用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用
- 使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)
- 页面重复刷新
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单
- 使用浏览器历史记录重复提交表单
- 浏览器重复的HTTP请求
- 定时任务重复执行
- 用户双击提交按钮
3.如何保证接口幂等性?
解决办法分为两个方向,一个方向是客户端防止重复调用,一个是服务端进行校验。当然,客户端防止重复提交并不是绝对可靠的,优点是实现起来比较简单。
1.按钮只可操作一次
一般是提交后把按钮置灰或loding状态,消除用户因为重复点击而产生的重复记录,比如添加操作,由于点击两次而产生两条记录
2.token机制
功能上允许重复提交,但要保证重复提交不产生副作用,比如点击n次只产生一条记录,具体实现就是进入页面时申请一个token,然后后面所有的请求都带上这个token,后端根据token来避免重复请求。
1)服务端提供获取token接口,供客户端进行使用。服务端用UUID生成token后,将token存放于redis中。
2)当客户端获取到token后,会携带着token发起请求。
3)服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,就是第一次提交请求,则先删除token再进行业务处理,如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。我们当时用lua脚本确保判断token是否存在若存在则删除token返回1否则返回0的操作原子性,不存在多个线程的并发问题,保证不会出现重复的提交。若业务处理异常则让客户端重新获取令牌,重新发起一次访问即可。
其实这里有个细节,其实我们第一次提交请求时无论是先进行业务处理还是先删除token都有点问题。若先业务处理再删除token,在高并发下,很有可能出现第一次访问时token存在,完成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。解决方案是对于业务代码执行和删除token整体加线程锁。
若先删除token再执行业务处理,假设具体业务代码执行超时或失败,没有向客户端返回明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进行业务处理。解决方案是一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。我们的项目里面用的是先删除token再执行业务的方案。
import redis.clients.jedis.Jedis;
import java.util.UUID;//用于生成唯一的订单令牌。
public class OrderService {
private Jedis jedis;//用于存储与Redis数据库的连接。
//分别表示Redis中的键前缀和订单令牌的过期时间(1小时)。
private static final String REDIS_KEY = "order_token:";
private static final int TOKEN_EXPIRE_TIME = 60 * 60; // 1小时
public OrderService(Jedis jedis) {
this.jedis = jedis;
}
//用于生成一个新的订单令牌
public String generateOrderToken() {
//使用UUID.randomUUID()方法生成一个随机的唯一标识符,并将其转换为字符串形式。
String token = UUID.randomUUID().toString();
jedis.setex(REDIS_KEY + token, TOKEN_EXPIRE_TIME, "1");
return token;
}
//用于检查给定的令牌是否已经提交
public boolean isOrderSubmitted(String token) {
//使用jedis.exists()方法检查Redis中是否存在以REDIS_KEY为前缀、加上给定令牌的键。如果存在,则返回true,表示订单已提交;否则返回false。
return jedis.exists(REDIS_KEY + token);
}
//用于删除给定的订单令牌
public void deleteOrderToken(String token) {
jedis.del(REDIS_KEY + token);
}
}
3.使用唯一索引防止新增脏数据
利用数据库唯一索引机制,当数据重复时,插入数据库会抛出异常,保证不会出现脏数据。