后端耗时接口处理
问题背景
后端有些接口执行时间特别长,有可能会在几分钟左右,但是前端接口会在1分钟超时。需求又需要后端执行完后返回结果。
处理方案(一般就是前端循环调和WebSocket两种,本文采用前端循环调)
- 后端异步执行,直接返回。
- 定义一个注解,加载方法上,表明采用该方案处理耗时任务。
- 前端可在请求时,传一个唯一性ID。后端在异步返回时,给一个商议好的code,表用需要前端循环调用来处理耗时接口,并给出建议循环时间。
- 定义Spring AOP 将注解和请求拦截。在异步返回时,将traceID作为key,一个设计好的返回对象作为value(前端收到后知道循环该继续还是结束),存进redis。
- 后端执行完后,将需要返回的数据,替换上一步key的value。
- 前端执行一个特殊接口,专门用于处理长接口的循环调用。在长接口成功返回后,删除该key。
具体简单代码实现
定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LongTimeAnnotation {
}
注解AOP处理
@Slf4j
@Aspect
@Component("LongAspect")
@RequiredArgsConstructor
public class LongAspect {
private IdWorker idWorker = new IdWorker();
@Resource
private final RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.moon.myredis.annotion.LongTimeAnnotation)")
public void longPointcut() {
}
@Pointcut("execution( public * com.moon.myredis.controller.LongTimeController.longTime(com.moon.myredis.vo.LongTimeReq))")
public void baseReqPointcut() {
}
@Before("longPointcut()")
@SneakyThrows(Throwable.class)
public void handleLongBefore(JoinPoint pj) {
Object[] args = pj.getArgs();
if (args.length > 0) {
BaseReq baseReq = (BaseReq) args[0];
log.info("长任务key: {}",baseReq.getTraceId());
redisTemplate.opsForValue().setIfAbsent(String.valueOf(baseReq.getTraceId()), RestResult.failed(ResultCodeEnum.WAIT_DATA));
}
}
@Around("longPointcut()")
@SneakyThrows(Throwable.class)
public Object handleLongAfter(ProceedingJoinPoint pj) {
Object[] args = pj.getArgs();
if (args.length > 0) {
BaseReq baseReq = (BaseReq) args[0];
Object result = pj.proceed();
log.info("长任务key: {}, value,{}",baseReq.getTraceId(),result);
redisTemplate.opsForValue().setIfPresent(String.valueOf(baseReq.getTraceId()), RestResult.success(result));
}
return pj.proceed();
}
@Before("baseReqPointcut()")
@SneakyThrows(Throwable.class)
public void handleBaseReq(JoinPoint pj) {
Object[] args = pj.getArgs();
if (args.length > 0) {
BaseReq baseReq = (BaseReq) args[0];
long nextId = idWorker.nextId();
baseReq.setTraceId(nextId);
pj.getArgs()[0] = baseReq;
}
}
由于简单测试使用,前端的唯一Id由后端拦截生成,在调用长接口时的traceId后端返回。
前端循环调用接口
@PostMapping("/loop")
public RestResult loop(@RequestBody LongTimeReq longTimeReq) {
return RestResult.success(longTimeService.getData(longTimeReq.getMessage()));
}
@Service
@Slf4j
public class LongTimeServiceImpl implements LongTimeService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public Object getData(String message) {
RestResult result = new RestResult();
result = (RestResult)redisTemplate.opsForValue().get(message);
log.info("key:{},value:{}",message, JSON.toJSONString(result));
if (result.getstatusCode() == 0){
return result;
}else {
redisTemplate.delete(message);
return result;
}
}
}
本次测试接口
@Resource
private LongTimeService longTimeService;
@PostMapping("/longTime")
public RestResult longTime(@RequestBody LongTimeReq longTimeReq) {
CompletableFuture.runAsync(() -> {
//模拟耗时操作
longTimeService.testLong(longTimeReq);
});
RestResult success = RestResult.success(longTimeReq.getTraceId());
success.setAsync(true);
return success;
}
@Override
@LongTimeAnnotation
@SneakyThrows(Exception.class)
public Object testLong(LongTimeReq longTimeReq) {
Thread.sleep(20_000);
return "成功";
}
RestResult的布尔值async属性像前端表明是一个长接口,需要循环调用。
测试
- 模拟前端循环处理
2. 循环处理成功