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秒
(留点余量,大家可以自行调整),接口就会退出
网友评论