Mybatis拦截器新玩法

优势

  • 真正实现可插拔,直接注入spring bean即可使用,对业务代码毫无侵入性


@Bean
public AutoExplainInterceptor autoExplainInterceptor() {
    return new AutoExplainInterceptor();
}
  • 真正实现配置化,是否生效以及如何生效完全由你决定,即使注册了spring bean,也能控制其是否生效


/**
* 是否自动生成执行计划,默认为false
*/
@Value("${mybatis.enable.auto-explain:false}")
private Boolean autoExplain;
/**
* 需要推送告警信息的类型关键字,多个以英文逗号分割
*/
@Value("${mybatis.need.warn-msg-type:ALL}")
private String needPushWarnMsgTypeKeyWords;
/**
* 扫描行数告警阈值,默认为10000
*/
@Value("${mybatis.scan.rows-threshold:10000}")
private Long scanRowsThreshold;
  • 真正实现高拓展,告警信息通过spring事件发布,可任意注册监听告警发布事件,自定义推送告警方式,插件默认通过邮件推送告警(可禁用)


/**
* 发布事件异步推送告警
*
* @param warnMsg
*/
private void publishWarnMsgEvent(String warnMsg) {
    applicationContext.publishEvent(new PushWarnMsgEvent(new PushWarnMsgVO().set WarnMsg(warnMsg)));
}

写此插件的初衷

咱们在工作中常常会接手一些陈年老代码,这些代码的年纪可能比咱们自己的年纪都要大,接手前可能一直没出坑对吧,然后咱们接手后,开始维护的时候,可能一些代码问题咱们能够提前暴露,但是一些关联查询,没加索引的sql,咱们在测试环境可以说很难发现,因为测试环境数据较少,那这个时候如果有一个插件能自动分析执行计划并推送告警给到咱们,那就避免问题sql发布线上,引发生产问题。所以,基于这个想法,设计并开源了此插件。
Q:MySQL不是有慢SQL统计,并且也能告警吗?
A:各位可以想想,咱们线上数据库是不是全部由DBA进行统一管理,就算DBA愿意为每个业务线配置数据库告警阈值,那也只能通过邮件的方式发送,这还不考虑公司大小以及人员流动的问题。咱们目前应该都是以实时通讯为主,例如企业微信/钉钉告警,这种一般最实时,并且咱们也能以最快的速度关注到,那如果咱们想通过企业微信/钉钉告警,MySQL能自动实现吗?不能呀,所以此插件应势而生。
Q:在执行查询SQL前都执行explain,不会有性能损耗吗?
A:答案是肯定的,不管explain执行有多快,它依然是同步执行的,会阻塞当前查询SQL,所以咱们插件设计的初衷就是挖掘一些潜在的风险,将风险及时在测试环境暴露,避免问题SQL上生产,所以咱们也是设计了一个开关,即使注入了AutoExplainInterceptor,我们依然可以在不同的环境控制其是否生效。如果生产环境查询QPS非常高,咱们可以让插件只在测试环境生效。

插件的实现思路

插件实现原理

重点源码跟踪

  • AutoExplainInterceptor实现


/**
 * 咱们拦截StatementHandler处理器的prepare,方便咱们获取Connection(数据库连接)以及ParameterHandler(参数设置对象)
 */
@Intercepts(
        @Signature(type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class})
)
//导入咱们需要的配置类,注入邮件发送监听者与springboot邮件发送对象
@Import(value = AutoInjectBeans.class)
public class AutoExplainInterceptor implements Interceptor {

    publicstatic final Logger LOGGER = LoggerFactory.getLogger(AutoExplainInterceptor.class);

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 是否自动生成执行计划,默认为false
     */
    @Value("${mybatis.enable.auto-explain:false}")
    private Boolean autoExplain;

    /**
     * 需要推送告警信息的类型关键字,多个以英文逗号分割
     */
    @Value("${mybatis.need.warn-msg-type:ALL}")
    private String needPushWarnMsgTypeKeyWords;

    /**
     * 扫描行数告警阈值,默认为10000
     */
    @Value("${mybatis.scan.rows-threshold:10000}")
    private Long scanRowsThreshold;

    /**
     * 需要推送告警信息的其他项关键字,多个以英文逗号分割
     */
    @Value("${mybatis.need.warn-msg-extra:Using filesort}")
    private String needPushWarnMsgExtraKeyWords;

    public Object intercept(Invocation invocation) throws Throwable {
        if (!autoExplain) {
            //if false,do nothing
           return invocation.proceed();
        }
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof StatementHandler) {
            StatementHandler statementHandler = (StatementHandler) target;
            BoundSql boundSql = statementHandler.getBoundSql();
            //原始sql
            String originSql = boundSql.getSql();
            if (!StringUtils.startsWithIgnoreCase(originSql, SqlCommandConstant.PREFIX_SELECT)) {
                //非查询语句,直接返回
                 return invocation.proceed();
            }
            //生成执行计划sql
            String explainSql = SqlHandleUtils.genExplainSql(originSql);
            //获取数据连接
            Connection connection = (Connection) args[0];
            //预编译执行计划sql
            PreparedStatement preparedStatement = connection.prepareStatement(explainSql);
            ParameterHandler parameterHandler = statementHandler.getParameterHandler();
            //对sql设置参数
            parameterHandler.setParameters(preparedStatement);
            //执行执行计划
            ResultSet resultSet = preparedStatement.executeQuery();
            List<ExplainResultVO> explainResultVoList = SqlHandleUtils.buildExplainResult(resultSet);
            if (CollectionUtils.isEmpty(explainResultVoList)) {
                thrownew IllegalArgumentException("execute explain error.");
            }
            //创建责任链分析执行计划结果
            AnalyzeExplainResultVO analyzeExplainResultVo = new AnalyzeExplainResultVO();
            analyzeExplainResultVo.setNeedPushWarnMsg(false);
            AnalyzeTypeHandler analyzeExplainHandlerChain = new AnalyzeTypeHandler(needPushWarnMsgTypeKeyWords,
                                                            new AnalyzeRowsHandler(scanRowsThreshold,
                                                            new AnalyzeExtraHandler(needPushWarnMsgExtraKeyWords,null)));
            analyzeExplainHandlerChain.analyze(explainResultVoList,analyzeExplainResultVo);
            //是否需要异步推送告警推送告警
            if (analyzeExplainResultVo.getNeedPushWarnMsg()) {
                String warnMsg = SqlHandleUtils.buildWarnMsg(SqlHandleUtils.formatSql(originSql),analyzeExplainResultVo.getErrMsg());
                LOGGER.error(warnMsg);
                this.publishWarnMsgEvent(warnMsg);
            }
        }
        return invocation.proceed();
    }

    /**
     * 发布事件异步推送告警
     *
     * @param warnMsg
     */
     private void publishWarnMsgEvent(String warnMsg) {
        applicationContext.publishEvent(new PushWarnMsgEvent(new PushWarnMsgVO().setWarnMsg(warnMsg)));
    }
}
Q:为什么解析执行计划结果需要使用责任链模式?看着好像比if-else复杂得多,并且每个链条都需要遍历一次执行计划结果?
A:咱们可以想想如果不使用责任链模式,会是怎么样,咱们的代码是不是都在一个方法里面,会使方法变的很长,影响代码可读性,再一个咱们目前虽然只分析了type/rows/Extra,那如果我们要拓展,是不是得继续写if-else,那么我们的方法是不是会更长,可读性是不是更差。然后虽然咱们每个链条都有遍历一次执行结果,但是我们采用的是for循环中最快的循环(fori,加强for,forEach,fori最快),这种遍历对性能的影响几乎可以忽略不计,所以基于此,我们需要采用责任链模式来提高可读性与拓展性。
  • AutoInjectBeans实现


@Configuration
public class AutoInjectBeans{

    @Bean
    @ConditionalOnProperty(prefix = "mybatis.enable.mail",name = "push-explain-warn",havingValue = "true")
    public PushWarnMsgByMailListener pushWarnMsgByMailListener(){

        returnnew PushWarnMsgByMailListener();
    }

    @Bean
    @ConditionalOnProperty(prefix = "mybatis.enable.mail",name = "push-explain-warn",havingValue = "true")
    public SendMailUtils sendMailUtils(){
        returnnew SendMailUtils();
    }

}
  • 咱们来梳理一下bean注入顺序

bean依赖注入顺序

插件自带邮箱告警功能

在application.yml中配置以下配置,在springboot环境下,spring-boot-starter-mail(插件已引入该依赖)会自动为咱们注入JavaMailSender,当使用邮箱告警功能时不要忘记将mybatis.enable.mail.push-explain-warn配置为true哟。

spring:
  mail:
    # SMTP 服务器 host  qq邮箱的为 smtp.qq.com 端口 465587host: smtp.qq.com
    # SMTP 服务器使用的协议
    protocol: smtp
    # SMTP服务器需要身份验证 所以 要配置用户密码
    # 发送端的用户邮箱名
    username: 邮箱用户名
    # 授权码
    password: 邮箱授权码
    # 接收人 多个以英文逗号分割
    receiver: xxx@163.com,yyy@qq.com
    #主题
    subject: SQL告警
    # 指定是否在启动时测试邮件服务器连接,默认为false
    test-connection: true
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true

SQL邮箱告警图

Q:告警sql不设置参数,这不是给排查添加难度吗?
A:没有设置参数原因有三,其一是咱们分析的是执行计划,参数在大多数情况下并不会影响执行计划的结果;其二是因为咱们explain会阻塞查询SQL的执行,设置参数又是增加另一负担;其三也是在其二的基础上,设置参数过程较为繁琐,给插件添加非必须代码。

自定义告警方式

如果有同学想自定义告警方式(监听spring事件#PushWarnMsgEvent),像咱们前面提到的,提倡使用企业微信/钉钉进行告警。那这个时候一定需要注意告警应该使用线程池异步推送告警,如有异常必须内部消掉,不能向上抛(CompletableFuture.runAsync有异常会往上抛哦),不能阻塞查询语句执行,因为起线程池会有资源占用,并且每个服务器的资源不一样,无法评估线程池参数配置,所以插件内部并没有内置线程池。

目前插件内部是使用CompletableFuture.runAsync,异步并消化掉异常处理,这个地方尤其注意


/**
     * 发送纯文本邮件.
     *
     * @param to      目标email 地址
     * @param subject 邮件主题
     * @param text    纯文本内容
     */
    public void sendMail(String to, String subject, String text) {
        CompletableFuture.runAsync(()->{
            try {
                SimpleMailMessage message = new SimpleMailMessage();
                message.setFrom(username);
                message.setTo(to.split(","));
                message.setSubject(subject);
                message.setText(text);
                javaMailSender.send(message);
                LOGGER.info("告警邮件发送成功");
            } catch (Exception e) {
                LOGGER.error(String.format("push warn msg mail fail,error msg:%s",e.getMessage()),e);
            }
        });
    }

所有代码已托管到github:
GitHub - NotExistUserName/auto-explain: 自动分析执行计划插件
插件仍存在优化之处,各位如果有在使用过程中,有任何槽点,欢迎私信评论告诉我,也可以直接发送邮件到 504401503@qq.com,我收到后如有必要我都会认真回复,最后呼吁大家一起拥抱开源,做出更好的东西大家一起学习讨论。如果各位同学觉得对你有所帮助,请关注、点赞、评论、收藏来支持我,手头宽裕的话也可以赞赏来表达各位的认可,各位同学的支持是对我最大的鼓励。未来为大家带来更好的创作。 

分享一句非常喜欢的话:把根牢牢扎深,再等春风一来,便会春暖花开。

版权声明:以上引用信息以及图片均来自网络公开信息,如有侵权,请留言或联系

504401503@qq.com,立马删除。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咖啡攻城狮Alex

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

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

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

打赏作者

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

抵扣说明:

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

余额充值