后端耗时接口处理

521 阅读2分钟

后端耗时接口处理

问题背景

后端有些接口执行时间特别长,有可能会在几分钟左右,但是前端接口会在1分钟超时。需求又需要后端执行完后返回结果。

处理方案(一般就是前端循环调和WebSocket两种,本文采用前端循环调)

  1. 后端异步执行,直接返回。
  2. 定义一个注解,加载方法上,表明采用该方案处理耗时任务。
  3. 前端可在请求时,传一个唯一性ID。后端在异步返回时,给一个商议好的code,表用需要前端循环调用来处理耗时接口,并给出建议循环时间。
  4. 定义Spring AOP 将注解和请求拦截。在异步返回时,将traceID作为key,一个设计好的返回对象作为value(前端收到后知道循环该继续还是结束),存进redis。
  5. 后端执行完后,将需要返回的数据,替换上一步key的value。
  6. 前端执行一个特殊接口,专门用于处理长接口的循环调用。在长接口成功返回后,删除该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属性像前端表明是一个长接口,需要循环调用。

测试

image.png

  1. 模拟前端循环处理

image.png 2. 循环处理成功

image.png