什么是幂等性?
幂等性是指一个操作或函数,无论执行多少次,其结果都是相同的。换句话说,重复调用具有幂等性的操作或函数不会产生额外的副作用或改变系统状态。
在计算机科学和网络通信中,幂等性是一个重要的概念。它确保无论请求被执行多少次,最终的结果都是相同的,不会因为重复执行而导致不一致或意外的行为。
举个例子,假设有一个幂等性的函数用于将某个数值存储到数据库中。无论调用这个函数一次还是多次,存储的值都会保持不变。这意味着多次调用函数不会导致数据库中出现多个相同的数值,因为幂等性保证了结果的一致性。
在实际场景中,可能出现前端未做防重处理或用户操作过快,重复请求导致数据异常,我们需要进行一些检查来保证幂等,保证同一个请求只被处理一次
如何实现请求的幂等性
以Java为例,我们需要实现如下效果
定义一个@Idempotent注解,只要将其加到需要保证幂等性的方法上,当发生重复请求时给出提示
实现原理为定义一个切面拦截所有加上@Idempotent注解的方法,将请求接口url加上进行MD5运算后的请求参数的值作为key,当重复的请求进来时,判断是否存在这个key,如果存在,则视为是同一个请求,返回错误提示。请求执行完成后,将key删除
1.定义注解
public @interface Idempotent {
/**
* 过期时间,单位:s,默认-1,即不设置过期时间
*/
long expireTime() default -1L;
/**
* 提示消息
*/
String message() default "请勿重复请求";
}
2.定义切面类
@Aspect
@Component
@AllArgsConstructor
public class IdempotentAspect {
private RedisUtils redisUtils;
private static ThreadLocal<String> idempotentKeyThreadLocal = new ThreadLocal<>();
@Pointcut("@annotation(cn.code4java.springbok.annotation.Idempotent)")
public void logPointCut() {
}
/**
* 处理前执行
*
* @param joinPoint
*/
@Before(value = "logPointCut()")
public void Before(JoinPoint joinPoint) {
handleAnnotation(joinPoint, null);
}
/**
* 处理后执行
*
* @param joinPoint
*/
@AfterReturning(pointcut = "logPointCut()", returning = "methodResult")
public void doAfterReturning(JoinPoint joinPoint, Object methodResult) {
// 删除key
String key = idempotentKeyThreadLocal.get();
if (key == null) {
return;
}
redisUtils.delete(key);
}
private void handleAnnotation(JoinPoint joinPoint, Exception e) {
Idempotent annotation = getAnnotation(joinPoint);
if (annotation == null) {
return;
}
String key = getIdempotentKey();
if (key == null) {
return;
}
if (redisUtils.hasKey(key)) {
throw new BusinessException(ExceptionEnum.PARAM_ERROR, annotation.message());
}
if (annotation.expireTime() > 0) {
redisUtils.set(key, "1", annotation.expireTime(), TimeUnit.SECONDS);
} else {
redisUtils.set(key, "1");
}
}
private Idempotent getAnnotation(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Idempotent.class);
}
return null;
}
private String getIdempotentKey(){
String requestURI = ServletUtils.getRequest().getRequestURI();
if (requestURI.substring(0,1).equals("/")){
requestURI = requestURI.substring(1);
}
requestURI = requestURI.replaceAll("/", ".");
// 请求体参数和url后携带的参数两者必须有一个不为空,若不携带参数则幂等注解不生效
String body = ServletUtils.getBody();
String queryString = ServletUtils.getRequest().getQueryString();
if (StringUtils.isBlank(body) && StringUtils.isBlank(queryString)) {
return null;
}
String params = (StringUtils.isNotBlank(body) ? body : "")
+ (StringUtils.isNotBlank(queryString) ? queryString : "");
String key = "idempotent:" + requestURI + ":" + MD5.create().digestHex(params);
idempotentKeyThreadLocal.set(key);
return key;
}
}