背景
对于慢SQL相信大家都不陌生了,一旦遇到后,相信大家会很快的提供出来对应的优化方法、索引优化建议工具使用等等,对于此我相信大家已经熟悉的不能再熟悉了,但是比较不尽人意的是:在此之前我们往往是花费了大量时间才发现造成系统出现问题的是慢SQL引起的,风险自然而然地也就慢慢升高了,基于此开发了一款简易的慢SQL自定义告警组件,为的就是提前预警,在影响扩大化之前进行快速止损,甚至是在一些新业务在上线前(测试、预发布)提前发现风险并规避
原理简介
通过mybatis拦截器进行sql语句执行过程的拦截,同步执行过程中计算sql执行的时间,当实际执行时间大于指定的配置阈值时发出告警信息并打印日志(日志中带有详细的调用链信息方便快速定位调用源头),同时将对应的sql放入异步队列disruptorQueue中,异步分析sql的执行计划,这里主要做3中场景预警
1.出现不走索引的全表扫描场景
2.没有走索引
3.扫描数据量超过配置阈值
关键代码
1、Mybatis拦截器实现执行时间超时预警以及放入异步队列
自定义一个类实现接口org.apache.ibatis.plugin.Interceptor,增加上官方提供的注解@Intercepts,这里主要介绍几个细节,具体关键代码如下
◦同步执行逻辑中根据sql的实际执行时间判断是否进行预警
◦预警日志中打印出具体的调用链堆栈信息
◦同一条sql在同一天最多只预警一次
◦具体sql的explain分析以及预警放在异步执行
◦开放子类可覆盖方法自行实现个性逻辑:比如mybatic-Plus的多数据源支持
/**
* 数据库操作性能拦截器,记录耗时
* @Intercepts定义Signature数组,因此可以拦截多个,但是只能拦截类型为:
* Executor
* ParameterHandler
* StatementHandler
* ResultSetHandler
*/
@Slf4j
@Component
@Intercepts(value = {
@Signature(type=Executor.class,
method="query",
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,
CacheKey.class,BoundSql.class}),
@Signature(type=Executor.class,
method="query",
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})
public class SqlInterceptor implements Interceptor {
/**
* 判定sql执行超时的时间标准,单位毫秒
*/
private final int DEFAULST_TIMEOUT = 1000;
/**
* sql最大扫描行数
*/
private final int DEFAULT_MAX_SCANROWS = 50000;
/**
* 每秒sql入队的最大限制
*/
private final double SQL_PERMITS_PERSECOND = 10.0;
/**
* 同一个sql在redis中的过期时间
*/
private final Long DEFAULT_REDIS_TIME = 24 * 60 *60L;
/**
* 分析sql阈值告警默认时间 单位:毫秒
*/
private final Long DEFAULT_EXPLAIN_ALERM_TIME = 0L;
/**
* 缓存时间 单位:秒
*/
private Long redisTime;
/**
* 分析sql阈值告警时间 单位:毫秒
*/
private Long explainAlermTime;
/**
* 限流器
*/
private RateLimiter rateLimiter = RateLimiter.create(SQL_PERMITS_PERSECOND,3*60, TimeUnit.SECONDS);
private String appCode;
private String appName;
private String appStackBasePackage;
private String dataSourceId;
private String redisClientId;
private Cluster redisClient;
private DataSource dataSource;
private String dbName;
private JdbcTemplate jdbcTemplate;
private Integer maxScanRows;
private Integer sqlTimeout;
private Boolean explainSwitch;
protected final Map<String,JdbcTemplate> jdbcTemplateMap = new ConcurrentHashMap<>();
/**
* 初始jdbcTemplateMap,可由子类实现
*/
public void initjdbcTemplateMap(){
}
/**
* 实现拦截的地方
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Method method = invocation.getMethod();
Object result = null;
if (target instanceof Executor) {
long start = System.currentTimeMillis();
/**执行方法*/
try{
result = invocation.proceed();
long end = System.currentTimeMillis();
long executeTime = end-start;
log.debug("sql性能监控-execute-target:{}毫秒",executeTime);
this.doTimeOutSql(invocation, executeTime);
}catch (Throwable var1) {
long end = System.currentTimeMillis();
long executeTime = end-start;
String logId = UUID.randomUUID().toString();
this.businessAlarmError(invocation, executeTime, logId);
throw new RuntimeException("sql性能监控-调用原始方法出错:logId=" + logId + ",方法:" + method, var1);
}
}
return result;
}
/**
* Plugin.wrap生成拦截代理对象
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 处理超时SQL
* 1.超时自定义报警
* 2.超时入redis慢SQL队列
* @param invocation
*/
private void doTimeOutSql(Invocation invocation,long executeTime){
long startTime = System.currentTimeMillis();
String preSql = "";
try{
SqlEventInfo sqlEventInfo = this.getRealSqlInfo(invocation, executeTime);
preSql = sqlEventInfo.getPreSql();
//自定义告警
List<StackTraceElement> pivotalStackTraces = getPivotalStackTraces();
sqlEventInfo.setPivotalStackTraces(pivotalStackTraces);
if(executeTime - getSqlTimeout() > 0){
StringBuffer alermKeyBuf = new StringBuffer();
alermKeyBuf.append(this.appCode).append(".").append(SqlAlermEnum.SQL_ALERM_KEY.getDesc());