OpenAPI 接口幂等实现

1、幂等性是啥?

进行一次接口调用与进行多次相同的接口调用都能得到与预期相符的结果。

通俗的讲,创建资源或更新资源的操作在多次调用后只生效一次。

2、什么情况会需要保证幂等性

比如,购物时的下单操作,如前端提交按钮未做并发、抖动控制,那么用户点击一次。可能因为某些原因导致 Http 请求了多次,这就会导致用户生成多个相同订单。

再有,在我们的分布式项目中,为了提高通行的可靠性,通信框架/MQ 可能会向数据服务推送多条相同的消息,如果不做幂等性控制,消息会被多次消费。

等等。。。

上述说了需要保证幂等性的场景,但我们实现幂等还要考虑下述条件:

  1. 如果服务接受了多个请求,且幂等 token请求参数完全一样,服务应该保证幂等直接返回相似数据。
  2. 如果服务接受了多个请求,且幂等 token请求参数不完全一样,服务应该拒绝幂等。

    即:幂等 token 不一致直接拒绝幂等直接走正常逻辑;

    幂等 token 一致但请求参数却不一致,我们返回 token 异常,也可以拒绝幂等。
  3. 不同用户之间的请求不能相互影响。
  4. 不同接口之间的请求不能相互影响。

    即:不同接口不能被相同 token 影响。
  5. 更新接口不能使用缓存数据,需要特殊处理。

    比如:客户端带了 幂等 token请求了会员续费接口,此时响应了新的会员过期时间,然后客户端又未携带了 幂等 token请求了会员续费接口,此时用户会员到期时间得到了更新,用户再次携带了 幂等 token 进行请求,响应的缓存的相似数据就明显不对了。
  6. 这里为啥说更新不能缓存,而创建未提呢?因为大多数更新需要考虑缓存一致性问题,而创建本身就是从无到有的过程,一般无需考虑,但也要根据实际业务来进行判断,这里后续实现方案为:创建直接走缓存,更新为重新查库。

3、如何保证幂等性

这里提供一种无侵入的幂等处理方案,构建幂等表

![image-幂等实现流程](/Users/yijun.wen/Library/Application Support/typora-user-images/image-20221024155920677.png)

流程解析:

  1. 客户端请求时,为相关接口(所有创建资源的接口、部分更新接口)添加一个请求头参数:clientToken ,

    clientToken 是一个由客户端生成的唯一的、大小写敏感、不超过64个 ASCII 字符的字符串。例如,clientToken=123e4567-e89b-12d3-a456-426655440000

    clientToken 可以由服务端提供单独的接口生成。生成方式很多这里不做讨论。

  2. 服务端对相关接口做 AOP 切入,处理进行幂等判断、幂等记录、数据缓存。

  3. 依据 Redis 中 clientToken 的状态信息返回相似信息

4、具体实现

4.1 创建切入点注解类

/**
* 幂等注解
*
* @author Eajur.wen
* @version 1.0
* @date 2022-10-19 11:25:09
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IdempotentAnnotation {
/**
* 是否缓存获取
*
* @return
*/
boolean cache() default true; /**
* 需要特殊处理的接口标识
*
* @return
*/
String name() default "";
}

cache 默认为 true ,会缓存第一次响应数据,后续幂等的请求直接走缓存响应数据

name 为需要特殊处理接口的标识,在 cache 为 false 时,根据此标识做特殊处理

4.2 幂等 AOP 实现

/**
* 幂等 AOP 实现
*
* @author Eajur.wen
* @version 1.0
* @date 2022-10-19 11:26:45
*/
@Component
@Aspect
@Slf4j
public class IdempotentAspect2 { public static final String CLIENT_TOKEN = "clientToken";
public static final String RENEWAL_NO_CACHE = "renewal";
public static final int CLIENT_TOKEN_MAX_LENGTH = 64;
public static final String CLIENT_TOKEN_KEY_PRE = "client:token:";
public static final String CLIENT_TOKEN_DATA_KEY_PRE = "client:token:data:";
public static final String CLIENT_TOKEN_DATA_ID_KEY_PRE = "client:token:data:id:";
public static final String CLIENT_TOKEN_DATA_ABSTRACT_KEY_PRE = "client:token:data:abstract:";
public static final long CLIENT_TOKEN_TIMEOUT_MINUTES = 5;
/**
* 请求中 处理中
*/
public static final int CLIENT_TOKEN_REQUEST_STATUS = 1;
public static final int CLIENT_TOKEN_SUCCESS_STATUS = 2; @Autowired
private HttpServletRequest request; @Autowired
private RedisTemplate redisTemplate; @Pointcut("@annotation(com.eajur.idempotent.annotation.IdempotentAnnotation)")
public void pt() {
} @Around("pt()")
public Object idempotent(ProceedingJoinPoint joinPoint) throws Throwable {
// 没有注解直接放行
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
IdempotentAnnotation annotation = method.getAnnotation(IdempotentAnnotation.class);
if (annotation == null) {
return joinPoint.proceed();
}
boolean cache = annotation.cache();
String clientToken = request.getHeader(CLIENT_TOKEN);
// 没有请求头直接放行
if (!StringUtils.hasText(clientToken)) {
return joinPoint.proceed();
}
// clientToken 不能过长
if (clientToken.length() > CLIENT_TOKEN_MAX_LENGTH) {
return new ViewData(ErrorCodeEnum.REPEATED_REQUEST_ERROR);
}
// 未登录接口暂不做幂等
Long memberId = SubjectUtil.getMemberId();
if (memberId == null) {
return joinPoint.proceed();
}
//获取参数名称和值
Map<String, Object> nameAndArgs = CommonUtil.getNameAndValue(joinPoint);
String jsonStr = JSONUtil.toJsonStr(nameAndArgs);
String abstractData = SmUtil.sm3(jsonStr); // 记录请求 clientToken
String methodName = method.getName();
String baseKey = memberId + ":" + methodName + ":" + clientToken;
String key = CLIENT_TOKEN_KEY_PRE + baseKey;
String dataKey = CLIENT_TOKEN_DATA_KEY_PRE + baseKey;
String abstractKey = CLIENT_TOKEN_DATA_ABSTRACT_KEY_PRE + baseKey;
ValueOperations ops = redisTemplate.opsForValue();
Object flag = ops.getAndExpire(key, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
if (flag == null) {
ops.set(key, CLIENT_TOKEN_REQUEST_STATUS, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
Object proceed;
try {
proceed = joinPoint.proceed();
} catch (Throwable throwable) {
// 请求失败清除幂等信息
redisTemplate.delete(key);
throw throwable;
}
ops.set(key, CLIENT_TOKEN_SUCCESS_STATUS, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
ops.set(abstractKey, abstractData, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
if (cache) {
ops.set(dataKey, proceed, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
}
return proceed;
}
// 请求参数不一致不做幂等
Object oldAbstractData = ops.getAndExpire(abstractKey, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
if (!abstractData.equals(oldAbstractData)) {
Object proceed;
try {
proceed = joinPoint.proceed();
} catch (Throwable throwable) {
// 请求失败清除幂等信息
redisTemplate.delete(key);
redisTemplate.delete(dataKey);
redisTemplate.delete(abstractKey);
throw throwable;
}
ops.set(key, CLIENT_TOKEN_SUCCESS_STATUS, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
ops.set(abstractKey, abstractData, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
if (cache) {
ops.set(dataKey, proceed, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
}
return proceed;
} // 上次请求未完成
if (flag.equals(CLIENT_TOKEN_REQUEST_STATUS)) {
return new ViewData().error(ErrorCodeEnum.REPEATED_REQUEST_ERROR);
} // 响应相似数据并刷新过期时间
if (flag.equals(CLIENT_TOKEN_SUCCESS_STATUS) && cache) {
Object data = ops.getAndExpire(dataKey, CLIENT_TOKEN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
return data;
} else {
String name = annotation.name();
switch (name) {
case RENEWAL_NO_CACHE:
// 特殊处理 我在这的处理是直接查库获取最新数据返回
// 可以通过 CLIENT_TOKEN_DATA_ID_KEY_PRE 缓存主键信息,也可以根据上面的 nameAndArgs 做处理
return new ViewData();
default:
return joinPoint.proceed();
}
}
}
}

4.3 在需要幂等的接口 Controller 方法上添加 @IdempotentAnnotation 注解即可

OpenAPI 接口幂等实现的更多相关文章

  1. 本页面用来演示如何通过JS SDK,创建完整的QQ登录流程,并调用openapi接口

    QQ登录将用户信息存储在cookie中,命名为__qc__k ,请不要占用 __qc__k : 1) :: 在页面顶部引入JS SDK库: 将“js?”后面的appid参数(示例代码中的:100229 ...

  2. Api接口幂等设计

    1,Api接口幂等设计,也就是要保证数据的唯一性,不允许有重复. 例如:rpc 远程调用,因为网络延迟,出现了调用了2次的情况. 表单连续点击,出现了重复提交. 接口暴露之后,会被模拟请求工具(Jem ...

  3. Spring Boot 接口幂等插件使用

    幂等概述 幂等性原本是数学上的概念,即使公式:f(x)=f(f(x)) 能够成立的数学性质.用在编程领域,则意为对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的. 幂等性 ...

  4. 阿里云openapi接口使用,PHP,视频直播

    1.下载sdk放入项目文件夹中 核心就是aliyun-php-sdk-core,它的配置文件会自动加载相应的类 2.引入文件 include_once LIB_PATH . 'ORG/aliyun-o ...

  5. springboot+redis+Interceptor+自定义annotation实现接口自动幂等

    前言: 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同.按照这个含义,最终的含义就是 对数据库的影响只能是一次性的 ...

  6. graphql-binding openapi 集成demo

    类似的将openapi 转换为graphql api 的也有 https://github.com/yarax/swagger-to-graphql 基本项目 参考代码 https://github. ...

  7. 防盗链&CSRF&API接口幂等性设计

    防盗链技术 CSRF(模拟请求) 分析防止伪造Token请求攻击 互联网API接口幂等性设计 忘记密码漏洞分析 1.Http请求防盗链 什么是防盗链 比如A网站有一张图片,被B网站直接通过img标签属 ...

  8. openapi and light-4j

    light-4j项目支持openapi规范,本文介绍一下参照相关demo做的上传功能. openapi.yaml,按照规范编写内容,/openapi/swagger可以查看对应的swagger页面,A ...

  9. Kubernetes官方java客户端之六:OpenAPI基本操作

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

随机推荐

  1. Apache DolphinScheduler&TiDB联合Meetup | 聚焦开源生态发展下的应用开发能力

    在软件开发领域有一个流行的原则:Don't Repeat Yourself(DRY),翻译过来就是:不要重复造轮子.而开源项目最基本的目的,其实就是为了不让大家重复造轮子. 尤其是在大数据这样一个高速 ...

  2. codeforces600E Lomsat gelral【线段树合并/DSU】

    第一次AC这道题,是三年前的一个下午,也许晚上也说不定.当时使用的\(DSU\) \(on\) \(tree\)算法,如今已经淡忘,再学习新的算法过程中,却与旧物重逢.生活中充满不可知会的相遇,即使重 ...

  3. MySQL 数据查询语句

    一般查询 字段取别名 别名不用加单引号,as 可省略. select t.id ID, t.name 名称 from grade t; 拼接字符串 concat(a, b) select concat ...

  4. Python图像处理丨基于OpenCV和像素处理的图像灰度化处理

    摘要:本篇文章讲解图像灰度化处理的知识,结合OpenCV调用cv2.cvtColor()函数实现图像灰度操作,使用像素处理方法对图像进行灰度化处理. 本文分享自华为云社区<[Python图像处理 ...

  5. C++ 遍历磁盘文件 非递归方法 和递归方法

    1: 非递归方法:  一起学习 寻找快乐 // File Name: CSearch.h #pragma once #include <vector> #include <atlst ...

  6. 使用JMeter测试.Net5.0,.Net6.0框架下无数据处理的并发情况

    1.   安装JMeter及使用 1.1下载JMeter 登录官方网站找到下载链接进行下载:https://jmeter.apache.org/download_jmeter.cgi 1.2配置环境变 ...

  7. Android 自动取色并设置沉浸式状态栏

    Android 自动取色并设置沉浸式状态栏 - Stars-One的杂货小窝 最近在进行产品的优化,也是研究了下沉浸式状态栏的实现方法及自动取色,记录一下笔记 设置沉浸式状态栏 1.添加依赖 这里,是 ...

  8. c++基础思维导图2

    c++基础思维导图2 结构体 结构体的基本概念:用户自定义的数据类型 结构体定义和使用 struct 结构体名{结构体成员} struct 结构体名 变量名: struct 结构体名 变量名 = {成 ...

  9. DispatcherServlet 分发流程

    0 太长不看版 HTTPServlet 的 Service 方法将请求按类进行分解 主要是根据HTTP方法的类型调用 doXXX 方法 GET 和 HEAD 方法需要对 if-modified-sin ...

  10. 【长文详解】TypeScript、Babel、webpack以及IDE对TS的类型检查

    只要接触过ts的前端同学都能回答出ts是js超集,它具备静态类型分析,能够根据类型在静态代码的解析过程中对ts代码进行类型检查,从而在保证类型的一致性.那,现在让你对你的webpack项目(其实任意类 ...