Java 接口 限制请求时间

2019-06-13  本文已影响0人  Yellowtail

0x1 概述

我们公司采取了前后端分离的模式,这是背景
客户端调用后台接口的时候,他们对http请求设置了一个超时时间: 10
偶尔,后端由于各种问题,请求不能在 10秒内返回,客户端其实已经报错了,但是服务器还在老老实实的继续执行,浪费了服务器和数据库性能
特别的是,在后端代码没有写好的时候,比如查询某些数据的时候,过滤字段没有加索引,那么数据库就会全表扫描,
当数据量比较大的时候,很容易花的时间会比较久,时间一旦超过了客户端的阈值,客户看到报错了,就会再次请求一次
如此往复,数据库就在短短的时间内CPU飙到了100%
进而导致整个系统全部变慢,所有接口超时,用户就会多次重试
恶性循环。。

0x2思路

所以,我想了一个办法,在接口过来的时候,在 filter 里面设置一个 时间戳,也就是请求开始时间

然后对 DAO 的每个方法就进行切入,每次进行数据库操作前,判断一下当时时间,和开始时间的时间差,
是否已经达到了阈值,如果是,就抛异常,快速结束这个请求;如果没有就放行

当接口提前退出的时候,其实用户还是会重试,只是比之前好点,因为之前我们有些接口可能会一直执行下去,
最后花了4~5 分钟才执行完,这个过程其实没有必要,就算我们执行完了,客户端也收不到我们的结果了,早点止损比较好

这个方法其实不能解决超时的问题,只是作为一种快速止损防止由点及面的故障扩散

0x3 TimeStamp

在思路里提到了 时间戳 TimeStamp
如果用普通的思路,从 filter传递一个参数到 切面里,对代码侵入性太大,所以我们需要一个工具类,可以借助 ThreadLocal 来实现

public class ThreadLocalUtils {
    
    private static ThreadLocalUtils INSTANCE = new ThreadLocalUtils();
    
    //请求开始时的时间戳
    private static ThreadLocal<Long> startTimeStamp = new ThreadLocal<Long>() {
        //涉及到 Long 到 long 的转换,所以设置一个默认值
        @Override
        protected Long initialValue() {
            return 0L;
        }
    };
    
    
    /**
     * <br>设置 时间戳
     *
     * @param value
     * @author YellowTail
     * @since 2019-06-10
     */
    public static void setTimeStamp(long value) {
        startTimeStamp.set(value);
    }
    
    /**
     * <br>得到时间戳
     *
     * @return
     * @author YellowTail
     * @since 2019-06-10
     */
    public static long getTimeStamp() {
        return startTimeStamp.get();
    }
    
    /**
     * <br>清除 时间戳
     *
     * @author YellowTail
     * @since 2019-06-10
     */
    public static void clearTimeStamp() {
        startTimeStamp.remove();
    }
}

0x4 filter

挑第一个 filter 设置一下 时间戳

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    
    //给当前线程设置 开始时间戳
    long start = System.currentTimeMillis();
    ThreadLocalUtils.setTimeStamp(start);
    
    LOGGER.info("request is start {}", start);
    
    chain.doFilter(request, response);
    
    //清理时间戳
    ThreadLocalUtils.clearTimeStamp();
    
}

0x5 切面

@Component
@Aspect
public class TimeOutAspectjService {

    private static final Logger LOGGER = LoggerFactory.getLogger(UnitModifyAspectjService.class);
    
    //7秒
    public static final long LIMIT = 7 * 1000;
    
    @Pointcut("execution(* com.xxx.MongoDAOSupport.*(..) ) ")
    private void invokeDao() {}
    
    //环绕类型,可以自行决定执行方法的时机
    @Around("invokeDao()")
    public Object updateUnit(ProceedingJoinPoint joinPoint) throws Throwable {
        long timeStamp = ThreadLocalUtils.getTimeStamp();
        
        if (0 != timeStamp) {
            long gap = System.currentTimeMillis() - timeStamp;
            if (gap >= LIMIT) {
                
                LOGGER.error("DAO methods exceed time limit, start is {}, gap is {}", timeStamp, gap);
                
                //抛异常,结束当前请求
                throw new BizExcetion(ErrorMsg.TIME_OUT);
            }
        }
        
        //有异常抛出来,这里不捕获
        return joinPoint.proceed();
    }
}

注意,com.xxx.MongoDAOSupport 是我们所有 DAO 类的父类方法

至此,所有的开发完成,一旦请求时间超过7秒(留点余量,大家可以自行调整),接口就会退出

上一篇 下一篇

猜你喜欢

热点阅读