什么是接口幂等性
要实现接口幂等性,首先我们需要了解什么是接口幂等性。当同一个接口被重复请求时,只允许执行一次我们称之为幂等性。那么为什么会出接口幂等性这一问题呢,可能由于网络问题而导致出现重复请求,或者由于恶意多次发送请求,导致出现接口幂等性等等其他原因。值得注意的吗,在HTTP方法中,正常情况下,GET,HEAD,PUT和DELETE 等方法都是幂等的,而POST方法是不支持幂等的,因此,大部分可能需要实现接口幂等性的都为POST。
怎么实现接口幂等性
- 使用唯一标识符(ID): 为每个请求分配唯一的ID,并在服务器端记录已经处理的ID。当接收到重复的请求时,服务器可以检查ID是否已经被处理,如果已经处理过则返回相同的结果,如果没有则进行处理。这种方法适用于需要在多次请求之间保持状态的情况,例如创建资源。
- 使用HTTP方法和URL设计: 合理设计HTTP接口的URL和使用HTTP方法可以帮助实现幂等性。GET、HEAD、PUT、DELETE等HTTP方法通常被认为是幂等的,而POST方法通常不是。使用PUT来更新资源,使用DELETE来删除资源,可以保持幂等性。
- 使用版本控制: 在接口中引入版本控制可以帮助实现幂等性。每个请求都带有一个版本号,服务器可以检查请求的版本号和已经处理的版本号是否一致,如果一致则返回相同的结果,如果不一致则处理请求。
- 使用乐观锁定: 对于需要更新资源的操作,可以使用乐观锁定来确保幂等性。客户端在每次请求中提供一个版本号或标记,服务器在更新资源时检查这个标记是否匹配当前资源的版本。如果匹配,才会执行更新操作。
- 使用幂等性标记: 在请求中包含一个幂等性标记,服务器在处理请求时检查这个标记,并根据标记的值来判断是否执行相同的操作。这个标记可以是一个自定义的HTTP头或请求参数。
- 使用事务处理: 对于需要执行多个操作的请求,可以使用事务处理来确保幂等性。事务可以保证一组操作要么全部执行成功,要么全部失败。
- 使用幂等性令牌: 为每个请求生成一个唯一的幂等性令牌,并在服务器端记录已经使用过的令牌。当接收到重复的请求时,服务器可以检查令牌是否已经被使用过,如果已经使用过则返回相同的结果,如果没有则进行处理。
利用SpringBoot实现接口幂等性
在本文中实现接口幂等性的操作主要基于生成唯一的token令牌并且将其令牌存入redis的方式来实现接口幂等性,并利用注解的方式实现接口幂等性操作。
- 导入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.1.3</version>
</dependency>
在application.yaml中导入:
spring:
data:
redis:
database: xx
host: xx.xx.xx
- 实现注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
long expireTime() default 10000;
}
- 生成唯一token令牌
@Service
@Slf4j
public class TokenService {
@Resource
private StringRedisTemplate stringRedisTemplate;
public boolean checkToken(String methodName, long expireTime, String reqJsonParam){
final boolean isDuplicate;
String uniqueParam = jdkMD5(reqJsonParam);
String redisKey = "token:Method="+methodName+"Param="+uniqueParam;
log.info("redisKey:{}", redisKey);
//超时时间的时间戳
long expireAt = System.currentTimeMillis() + expireTime;
String val = "expireAt@" + expireAt;
log.info("expireAt:" + expireAt);
if (Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(redisKey, val))) {
if (Boolean.TRUE.equals(stringRedisTemplate.expire(redisKey, expireTime, TimeUnit.MILLISECONDS))) {
isDuplicate = false;
} else {
isDuplicate = true;
}
} else {
log.info("加锁失败 failed!!key:{},value:{}",redisKey,val);
return true;
}
return isDuplicate;
}
private static String jdkMD5(String src) {
String res = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] mdBytes = messageDigest.digest(src.getBytes());
res = DatatypeConverter.printHexBinary(mdBytes);
} catch (Exception e) {
log.error("",e);
}
return res;
}
}
- 在注解的切面实现类中实现其幂等性
@Aspect
@Component
@Slf4j
public class IdempotentAspect {
@Resource
private TokenService tokenService;
@Pointcut("@annotation(com.example.demo.annotation.Idempotent)")
private void pointCut(){}
@Around("pointCut()")
public Object print1(ProceedingJoinPoint joinPoint) throws Throwable {
boolean check = this.handleRequest(joinPoint);
if(check){
//重复请求,提示重复 报错
log.info("重复性请求..");
throw new Exception("请求重复");
}
return joinPoint.proceed();
}
private Boolean handleRequest(ProceedingJoinPoint joinPoint) {
boolean result;
log.info("========判断是否是重复请求=======");
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取自定义注解值
Idempotent idempotent = methodSignature.getMethod().getDeclaredAnnotation(Idempotent.class);
long expireTime = idempotent.expireTime();
// 获取参数名称
String methodsName = methodSignature.getMethod().getName();
String[] params = methodSignature.getParameterNames();
//获取参数值
Object[] args = joinPoint.getArgs();
Map<String, Object> reqMaps = new HashMap<>();
for(int i=0; i<params.length; i++){
reqMaps.put(params[i], args[i]);
}
log.info("INFO:"+ reqMaps);
String reqJSON = JSON.toJSONString(reqMaps);
result = tokenService.checkToken(methodsName, expireTime, reqJSON);
return result;
}
}
- 验证
@RestController
public class demoController{
@Idempotent(expireTime = 50000)
@PostMapping("/hello")
public String Get(@RequestBody String a){
System.out.println(a);
return "hello";
}
}