一、思路分析
在调用后台接口时,由于用户多次点击或者说第三方重试,可能会导致幂等问题。
解决方案无非就是上一次请求没有处理完,第二次请求不会处理,或者直接提示请求频繁,让用户等待。
我们基于SpringAOP(或者拦截器)来实现接口的幂等处理,多次请求时,提示用户不要重复请求
,并缓存处理结果,将处理后的结果快速返回。
流程图如下:
二、代码实战
1、搭建Springboot+AOP+Redis环境
略
2、自定义注解
该注解标注在Controller层,可以根据项目需要进行参数调整,比如说可以实现按指定字段判断幂等、实现接口的限流、指定幂等的判断条件等等。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 实现幂等的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}
3、切面类
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 幂等切面
*/
@Aspect
@Component
public class IdempotentAspect {
private final StringRedisTemplate redisTemplate;
private final String Status = "status";
private final String Begin = "begin";
private final String End = "end";
private final String Data = "data";
@Autowired
public IdempotentAspect(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable {
String identifier = generateIdentifier(point);
// 直接设置状态为begin,设置成功,说明该请求已处理
Boolean beginStatus = redisTemplate.opsForHash().putIfAbsent(identifier, Status, Begin);
if(!beginStatus) {
// 先取数据,再取状态,防止这期间数据过期
Object dataObject = redisTemplate.opsForHash().get(identifier, Data);
Object status = redisTemplate.opsForHash().get(identifier, Status);
if (Begin.equals(status)) {
// 请求在处理,抛异常退出
throw new RuntimeException("请求处理中,不要重复请求");
}
if (End.equals(status)) {
// 如果处理结束,直接将结果返回
return JsonAide.fromJson(dataObject.toString(), getMethod(point).getReturnType());
}
}
// 5分钟过期
redisTemplate.expire(identifier, 5, TimeUnit.MINUTES);
// 设置成功,执行流程
Object proceed = point.proceed();
// 将请求状态设为结束,并且缓存返回值
// TODO 请求结束之后,可以将key删掉,在业务中进行判断是否重复请求
redisTemplate.opsForHash().put(identifier, Status, End);
redisTemplate.opsForHash().put(identifier, Data, JsonAide.toJson(proceed));
return proceed;
}
/**
* 获取方法
*/
private Method getMethod(ProceedingJoinPoint point) {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
return methodSignature .getMethod();
}
/**
* 获取方法签名
*/
private String generateIdentifier(ProceedingJoinPoint point) {
// 获取方法参数和相关信息
Method method = getMethod(point);
Object[] args = point.getArgs();
// 根据方法名和参数生成唯一标识 TODO 可以替换为使用统一流水号
return method.getName() + ":" + Stream.of(args).filter(Objects::nonNull).map(Object::toString).collect(Collectors.joining());
}
}
4、测试一下吧
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test")
@Idempotent
public Object test(@RequestBody Map<String, String> req) throws InterruptedException {
System.out.println("请求进来了");
// 休眠10秒 ,模拟业务处理时间
Thread.sleep(10000);
System.out.println("请求处理结束了");
return req;
}
}
我们发现,同一个请求未处理完成,会抛异常,此时我们捕获这个异常即可。(或者使用拦截器实现)