手撕一个SQL拦截熔断功能

8ef6de101759946e414ab9fd97b17375.png

需求:

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

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

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

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

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

原理如下:

cd6a06499263536756bc305ed885a1af.png

实现步骤:

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关键字模板顺序匹配,利用了正则表达式解决,有其它需求的可以调整这块的匹配规则;

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

72dbabf2ea9eaed9872d0e09e3257a80.png

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕哥架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值