接口幂等性
在我们实际开发场景当中,很多时候都会有重复消费的业务场景出现.这是我们不希望出现的.例如我们在支付订单时,往往会遭受卡顿的情况,我们如果反复点击支付
,如何保持我们只消费一次的现象,成为了一个难题.
这时我们希望此时是数据更新是幂等的.
那什么是幂等性呢?
幂等是数学里面的一个概念.我们可以用函数表达式来表达
而运用到我们程序的开发过程当中,则是指在同一个业务场景下,执行一次或者多次对业务状态的影响都是一致的
.
那什么时候会引起这种幂等性的问题呢?
例如
- 网络波动, 可能会引起重复请求。
- 用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用。
- 使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)。(
也就是执行业务时突然阻塞,一段时间超时以后重试又发送一个请求或者一个消息重发导致重复消费
). - 使用浏览器后退按钮重复之前的页面操作,导致重复提交表单。
这导致我们需要幂等性的需求出现.
而有些业务天生就是幂等.
例如查询
时,我们查询一万次与查询一次的结果时是一样的.
还有就是删除一条数据时,我们删除过后,无论你再发多少请求过来,数据已经被删除了
,不存在重复删除这个问题.
而对于增加和更新这两个业务场景我们就不得不采取一些措施来保证接口的幂等性.
接下来模拟一下这些业务场景的出现.
我们先来模拟一下增加数据出现幂等性问题的原因.
我们首先创建一个user表,表结构如下
接下来构建三层架构.(技术栈为Spring Boot,Thymeleaf,Mybatis-Plus)
我们撰写一个简单的页面
新增页面
我们在添加用户的让线程睡上3秒,模拟网络卡顿的情况.
我们接下来疯狂点击增加,浏览器出现以下状态
然后页面跳转我们发现增加了许多记录
这就导致啦幂等性问题的出现.
那我们可以怎么处理呢?想想,我们想要它能够幂等就得保证唯一,我们能通过什么手段保证传过来这么多次是唯一的呢?
我们是不是可以模拟一个一字段,或者说一个标志,当你进入这个页面之后就会携带这样一个标志,无论你点击多少次都会带上这个标志,只不过第一次来的时候,我们就会把这个标志剔除
,那么接下来的请求我们判定你这样的标志在不在我这里存在.由于第一次来将标志已经剔除,后面的请求自然就是不存在
,所以我们就不会让他添加,构成幂等性的条件.
那我们得借用一下redis老哥啦,我们在进入这个页面随机生成一个UUID携带,然后配置一个拦截器,当我们请求添加时,拦截器拦下,检查是否携带这样一个标志,如果存在,我们剔除,如果不存在,我们直接打回.
改写原来的跳转接口逻辑
改写原来页面提交的参数
添加注解,用来声明需要拦截的方法
配置拦截器
@Component
@RequiredArgsConstructor
public class MyInterceptor implements HandlerInterceptor {
private final StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
final HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法
final Method method = handlerMethod.getMethod();
// 判断有没有添加需要幂等性注解
boolean methodAnn = method.isAnnotationPresent(ApiIdepotentAnn.class);
// 判断是否开启幂等性严重
if (methodAnn && method.getAnnotation(ApiIdepotentAnn.class).value()) {
// 需要实现接口幂等性
boolean result = checkToken(request);
if (result) {
return true;
} else {
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print("重复调用");
writer.close();
response.flushBuffer();
return false;
}
}
return true;
}
private boolean checkToken(HttpServletRequest request) {
String token = request.getParameter("token");
if (StrUtil.isEmpty(token)) {
// 没有token,说明重复调用或者
return false;
}
// 返回是否删除成功
return stringRedisTemplate.delete(token);
}
}
配置MVC环境
再次启动项目反复点击添加,发现第二次以后我们就会失败
数据只产生一条,完成幂等性要求
更新操作
而对于更新操作,我们就可以使用我们熟知的版本号啦.悲观锁影响性能,所以使用乐观锁可以提高我们的处理性能
我们针对年龄进行接口幂等性的模拟
我们先修改对应接口
/**
* 更新用户
* @param id 用户id
* @return
*/
Integer updateAge(Long id);
编写Mapper接口语句
<mapper namespace="com.itbaizhan.idempotentdemo.mapper.UserMapper">
<update id="updateAge" parameterType="long" >
update user set age = age + 1 where id = #{id}
</update>
</mapper>
回到页面我们对年龄进行更新,这里我们简单处理就是把年龄加一(可以联想一下转账或者消费的一些场景)
我们疯狂点击更新
年龄不受控制的多增加了很多次,幂等性问题出现
我们的处理也很简单,在跳转更新页面时拿到它的版本号,存入页面中,隐形的存储进去
接下来我们修改我们的mapper接口
此时我们回到页面,疯狂点击
结果认证,年龄只增加了1,完成幂等性.