天天看点

手撕一个SQL拦截熔断功能

手撕一个SQL拦截熔断功能

需求:

SQL熔断或者拦截作为保障数据库稳定的一种手段一般用在紧急解决数据库压力问题上,这在大多数云服务系统中都是适用的,

1)上线了慢SQL、缺索引等没有提前发现

2)数据库访问量大、压力较大需要紧急熔断一些平时性能差的SQL

不管你的系统平时有没有问题,作为架构师,都要有兜底手段,等真的出现问题时要能力挽狂澜,挽救系统。

在阿里云RDS服务中具有SQL限流熔断功能,相当于中间件代理实现的SQL拦截,今天老吕就手撕一个从应用层实现的SQL拦截熔断功能。

原理如下:

手撕一个SQL拦截熔断功能

实现步骤:

1、自定义Druid过滤器实现SQL拦截

/**
 * @Project 
 * @Description 问题sql拦截熔断
 * @Author lvaolin
 * @Date 2021/9/28 下午3:52
 */
public class MyDruidSqlInterceptorFilter extends FilterEventAdapter {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 缓存sql模板正则编译信息提高性能
     */
    private Map<String, Pattern> patternMap = new ConcurrentHashMap();
    /**
     * 配置中心服务,获取sql拦截启动开关和SQL黑名单信息(定时更新加载)
     */
    private ConfigService configService;


    public MyDruidSqlInterceptorFilter(ConfigService configService) {
        this.configService = configService;
    }


    @Override
    protected void statementExecuteBefore(StatementProxy statement, String sql) {
        //sql拦截开关
        if (configService.getCheckConfigDefault().getSqlInterceptorEnable()) {
            //排除配置表的访问拦截+正则匹配SQL黑名单模板
            if (sql != null && !sql.contains("sys_running_mode_config") && isMatch(sql)) {
                logger.error("SQL熔断通知:" + sql + "@" + ArrayUtils.toString(HealthCheckUtil.getCurrentTTKThreadTrace()));
                throw new BusinessException("", "SQL熔断通知:" + sql);
            }
        }


    }


    /**
     * 如原始语句SELECT min(id), max(id) FROM task_event
     * WHERE gmt_modified < '2020-06-21'
     * AND begin_time > '2020-07-09'
     * AND source IN (527)
     * AND id >= 15673
     * AND id <= 8015673 ,
     * 则对应该语句的限流关键词可以配置为
     * SELECT~min~id~max~id~FROM~task_event~WHERE~gmt_modified~AND~begin_time~
     * AND~source~IN~AND~id~AND~id
     * 加载时系统会将 ~替换为.*,另外在两端也会加 .*
     * 关键词有顺序约束,匹配时也会按顺序匹配
     *
     * @param sql
     * @return
     */
    private boolean isMatch(String sql) {
        try {
            for (String sqlTemplate : configService.getBlackSqlList()) {
                if (doMatch(sqlTemplate, sql)) {
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }


    /**
     * 正则匹配SQL与sql模板
     * @param sqlTemplate
     * @param sql
     * @return
     */
    private boolean doMatch(String sqlTemplate, String sql) {
        Pattern p = getPattern(sqlTemplate);
        Matcher m = p.matcher(sql);
        return m.matches();
    }


    /**
     * 缓存下正则编译信息提高性能
     * @param template
     * @return
     */
    private Pattern getPattern(String template) {
        Pattern pattern = patternMap.get(template);
        if (pattern == null) {
            pattern = patternMap.put(template, Pattern.compile(template));
        }
        return pattern;
    }


}
           

2、配置生效

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
   <property name="url" value="${jdbc.url}" />
   <property name="username" value="${jdbc.username}" />
   <property name="password" value="${jdbc.password}" />
   <property name="initialSize" value="1" />
   <property name="minIdle" value="1" />
   <property name="maxActive" value="20" />
   <property name="filters" value="config,log4j2" />
   <property name="proxyFilters">
      <list>
         <bean id="sqlInterceptor" class="com.dhy.healthcheck.util.MyDruidSqlInterceptorFilter">
            <constructor-arg type="com.dhy.healthcheck.eventlistener.ConfigService" ref="configService"/>
         </bean>
      </list>
   </property>
</bean>
           

总结:

1、总体来说实现起来还是比较轻松的,覆盖了statementExecuteBefore方法,拦截成功后直接抛出异常返回;

2、拦截开关和SQL黑名单配置信息的实时更新各位同学根据自己系统情况实现即可;

3、本方案采用的是sql关键字模板顺序匹配,利用了正则表达式解决,有其它需求的可以调整这块的匹配规则;

谢谢大家关注,喜欢的三连击。

手撕一个SQL拦截熔断功能