自定义Interceptor实现对Mybatis超时执行SQL的监控

MyBatis插件机制实战
本文介绍如何利用MyBatis的插件机制实现SQL执行效率监控及超时通知功能。通过定义拦截器并配置在特定方法上,可以计算SQL执行时间并在超过设定阈值时记录警告日志。

从事代码一年多了,遇到了很多的bug,也解决了很多bug,特此趁机记录一下:
      责任链设计模式是很多框架经常采用设计模式,一定程度上拥抱了面向对象的开放和扩展,给用户暴露接口的形式能够无侵入性让用户能够做一些系统性的工作。责任链设计模式 让这些对象形成一条链,并沿着这条链传递请求,直到链上的某一个对象决定处理此请求。
    在Spring中Aop中定义过切面Aspect可以实现对方法执行的前后等进行拦截,在Netty中ChannelPipeline中持有众多自定义的ChannelHandler来完成编解码,序列化和反序列化,拆包和其他业务功能,在Tomcat也采用责任链模式,Pipline拥有很多valve,实现Engine,Host,Context,Wrapper的处理和装饰功能。Servlet也使用Filter和Listener形成责任链模式,Log4j也是同样的模型。Dubbo等是通过Invoker来实现责任链模式。类似的框架太多了,希望以后能够详细见识。

     今天要说的基于动态代理的方式的Mybatis中的Plugin插件,这个可能比较陌生,虽然性能也据说不高,但还是在此演示下。首先思考三个问题,插件在哪里侵入代码?插件如何实现对原有功能和对象的代理和操作?插件要实现什么功能?

首先:配置文件中配置如下

 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
          p:dataSource-ref="dataSource"
          p:configLocation="classpath:mybatis-config.xml">
        <property name="plugins">
            <array>
                <bean class="com.XX.XX.aopmethod.NotifyInterceptor">
                    <property name="properties">
                        <value>
                            notifyTime=30
                        </value>
                    </property>
                </bean>
            </array>
        </property>
    </bean>
其次定义拦截器,
/**
 * Created by wisdom on 2018/6/13.
 */
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class})})
public class NotifyInterceptor implements Interceptor {
    private static final Logger LOGGER = LogManager.getLogger(NotifyInterceptor.class);
    private Properties properties;

    @Override
    public Object intercept(Invocation arg0) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) arg0.getArgs()[0];
        Object parameter = null;
        if (arg0.getArgs().length < 1) {
            parameter = arg0.getArgs()[1];
        }
        long start = System.currentTimeMillis();
        Object returnValue = arg0.proceed();
        long end = System.currentTimeMillis();
        long time = (end - start);
        String sql = this.getActualSql(mappedStatement.getConfiguration(), mappedStatement.getBoundSql(parameter),
                mappedStatement.getId(), time);
        if (time > Long.parseLong(String.valueOf(properties.get("notifyTime")))) {
            LOGGER.warn(sql);
        }

        return returnValue;
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    public String getActualSql(Configuration configuration, BoundSql boundSql, String sqlId, long time) {
        String sql = showSql(configuration, boundSql);
        StringBuilder str = new StringBuilder("执行缓慢的SQL:");
        str.append(sqlId);
        str.append(":");
        str.append(sql);
        str.append(":");
        str.append(time);
        str.append("ms");
        return str.toString();
    }

    public String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    private static String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            value = "'" + FormatUtil.dateToString((Date) obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

     该setProperties方法,这个方法在Configuration初始化当前的Interceptor时就会执行,这里存放超时时间。看MyInterceptor类上我们用@Intercepts标记了这是一个Interceptor,然后在@Intercepts中定义了两个@Signature,即两个拦截点。第一个@Signature我们定义了该Interceptor将拦截Executor接口中参数类型为MappedStatement、Object的update方法;第二个@Signature我们定义了该Interceptor将拦截Executor中参数类型为MappedStatement、Object,RowBounds和ResultHandler的query方法。方法内部通过计算方法执行前后的时间差,来衡量执行耗时,如果大于指定值,则可以报警和通知,不过这里为了简单,只是记录下来。运行效果如下:

com.XX.XX.getUserInfoByDsId:select top 1 * from UserInfo WITH(nolock) where DsId = 'test':5709ms(项目启动,处理缓慢)
com.XX.XX.getUserInfoByDsId:select top 1 * from UserInfo WITH(nolock) where DsId = 'test':10ms

   通过如上的代码,就可以实现监视每条SQL的执行效率,并且实现超时自动通知功能。

   同时也可以猜测各种框架的责任链模式使用都是大同小异,对用户来说,要做的开发不是很大,熟练掌握其API就可以完成功能。当然要对该模式的实现依赖的动态代理和持有的链表或者其他数据容器有所了解,就可以游刃有余啦。




<think>我们面对的是一个关于SpringBoot集成MyBatis-Plus和Druid时,配置SQL执行超时后自动重试的问题。根据用户需求,我们需要配置Druid连接池,使其在发生读取超时(可能由于网络波动等原因)时能够自动重试SQL执行。分析:1.Druid本身不提供SQL执行超时重试的功能,但我们可以通过其他方式实现,例如:-使用Spring的重试机制(如SpringRetry)-在应用层通过代码实现重试(例如使用AOP拦截特定异常进行重试)2.但是,用户希望的是在Druid配置中实现,这可能需要我们利用Druid的Filter机制或者结合其他配置。然而,查阅Druid文档,它并没有内置的SQL重试功能。因此,我们需要在应用层面实现重试。考虑到用户使用的是MyBatis-Plus,我们可以结合MyBatis-Plus的插件机制或者使用SpringRetry来实现。方案选择:1.使用SpringRetry:通过注解@Retryable来实现方法的重试,当方法抛出特定异常(如超时异常)时进行重试。2.使用自定义AOP:拦截Mapper接口的方法,捕获超时异常,进行重试。这里我们选择使用SpringRetry,因为它更简洁且易于配置。步骤:1.添加SpringRetry依赖。2.启用重试机制:在启动类上添加@EnableRetry。3.在需要重试的方法上添加@Retryable注解,指定重试的异常、重试次数等。但是,用户可能希望全局配置,而不是在每个方法上添加注解。我们可以通过AOP结合SpringRetry来实现全局重试,或者使用自定义的拦截器。另一种思路:MyBatis-Plus的插件机制。我们可以开发一个MyBatis的插件(Interceptor),在拦截到SQL执行异常时进行重试。这里我们提供一个使用MyBatis插件实现重试的示例。具体实现:1.自定义一个MyBatisInterceptor,拦截Executor的query和update方法。2.在拦截器中,捕获数据库操作异常(特别是超时异常),然后进行重试。注意:重试需要考虑幂等性,只有查询操作和幂等的更新操作才适合重试。由于用户特别提到“数据读取超时”,所以我们主要针对查询操作(SELECT)进行重试。配置示例:首先,我们创建一个重试拦截器:```java@Intercepts({@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,CacheKey.class,BoundSql.class}),@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class})})publicclassRetryInterceptorimplementsInterceptor{privatestaticfinalintMAX_RETRIES=3;//最大重试次数privatestaticfinallongRETRY_INTERVAL=1000L;//重试间隔(毫秒)@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{intretries=0;Throwablethrowable=null;while(retries<MAX_RETRIES){try{returninvocation.proceed();}catch(Throwablet){//判断是否为超时异常,这里以SQLTimeoutException为例if(isTimeoutException(t)){throwable=t;retries++;//等待一段时间再重试Thread.sleep(RETRY_INTERVAL);}else{throwt;}}}throwthrowable;//重试多次后仍然失败,抛出异常}privatebooleanisTimeoutException(Throwablet){//这里可以根据实际情况判断,比如判断是否为java.sql.SQLTimeoutExceptionreturntinstanceofSQLTimeoutException;}@OverridepublicObjectplugin(Objecttarget){returnPlugin.wrap(target,this);}@OverridepublicvoidsetProperties(Propertiesproperties){}}```然后,将这个拦截器配置到MyBatis的拦截器链中。在SpringBoot中,我们可以通过@Bean来注入:```java@ConfigurationpublicclassMyBatisConfig{@BeanpublicRetryInterceptorretryInterceptor(){returnnewRetryInterceptor();}}```但是,请注意:-这种重试机制会增加数据库的负载,特别是在数据库压力大的时候,所以重试次数不宜过多。-重试间隔需要根据实际情况调整。-只对查询操作和幂等的更新操作进行重试,非幂等的更新操作(如扣款)重试可能会导致重复执行。另外,关于Druid的配置,我们可以设置超时时间(通过数据库驱动配置,例如MySQL的socketTimeout),但是超时后的重试需要由应用层处理。Druid配置超时:-在Druid数据源配置中,可以通过连接属性设置socketTimeout(网络套接字超时)和connectTimeout(连接超时)。-例如:在application.properties中配置:spring.datasource.druid.connect-timeout=3000#连接超时3秒spring.datasource.druid.socket-timeout=5000#读取超时5秒当读取超时(socketTimeout)发生时,会抛出SQLTimeoutException,然后被我们的拦截器捕获并重试。总结:-我们通过自定义MyBatis拦截器实现SQL执行超时的重试。-同时配置Druid的连接超时和读取超时参数。但是,用户可能还需要考虑事务中的重试问题。在事务中,如果重试,整个事务可能已经超时,或者重试时连接可能已经不可用。因此,这种重试机制更适合于非事务性的读取操作,或者短事务。如果用户需要更强大的重试机制(如支持事务),可能需要考虑使用SpringRetry并结合事务管理器进行复杂配置,或者使用分布式事务框架。因此,我们提供的方案适用于简单的读取超时重试。对于复杂场景,需要更深入的设计。下面给出一个完整的配置示例(包括Druid的基本配置和自定义拦截器配置):1.添加依赖(除了MyBatis-Plus和Druid,还需要MyBatis的依赖,因为拦截器是MyBatis层面的):注意:MyBatis-Plus已经包含了MyBatis,所以不需要额外添加。2.配置Druid数据源(application.properties):注意:这里我们配置了连接超时和读取超时(通过连接属性),但注意MySQL驱动中是通过连接参数来设置socketTimeout的,所以我们可以通过Druid的connectionProperties来设置。示例配置:```properties#数据源基本配置spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=rootspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver#Druid连接池配置spring.datasource.type=com.alibaba.druid.pool.DruidDataSource#初始化大小,最小,最大spring.datasource.druid.initial-size=5spring.datasource.druid.min-idle=5spring.datasource.druid.max-active=20#配置获取连接等待超时的时间spring.datasource.druid.max-wait=60000#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒spring.datasource.druid.time-between-eviction-runs-millis=60000#配置一个连接在池中最小生存的时间,单位是毫秒spring.datasource.druid.min-evictable-idle-time-millis=300000#配置超时参数(通过连接属性设置)#连接超时(单位:毫秒)spring.datasource.druid.connect-timeout=3000#读取超时(单位:毫秒)-通过连接属性设置,因为MySQL驱动中是通过socketTimeout参数设置的spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=2000;socketTimeout=5000#其他Druid配置(如监控等)根据需求添加```注意:上面的socketTimeout=5000是通过connectionProperties设置的,这样MySQL驱动就会使用这个值作为读取超时。3.编写并注册上面提供的RetryInterceptor。4.注意:如果使用MyBatis-Plus,它已经内置了一些拦截器(如分页拦截器),我们自定义的拦截器需要确保顺序,但重试拦截器应该在最外层(最先执行,最后返回)。这样,当SQL执行超过5秒(即socketTimeout设置的时间)时,会抛出SQLTimeoutException,然后被重试拦截器捕获并重试,最多重试3次,每次间隔1秒。相关问题:1.如何调整重试次数和重试间隔?2.除了超时异常,还可以重试哪些异常?3.如何避免非幂等操作被重试?4.在事务中重试SQL需要注意什么?请根据实际需求调整重试策略。</think>### Spring Boot + MyBatis-Plus + Druid 实现 SQL 超时重试配置 在 Spring Boot 集成 MyBatis-Plus 和 Druid 时,可通过以下方案实现 SQL 读取超时的自动重试功能: #### 方案 1:Druid 连接池配置(基础防护) ```properties # application.properties # 启用超时重连 spring.datasource.druid.test-while-idle=true spring.datasource.druid.test-on-borrow=true spring.datasource.druid.test-on-return=false # 超时配置(单位:毫秒) spring.datasource.druid.max-wait=3000 # 获取连接超时时间 spring.datasource.druid.query-timeout=2000 # SQL执行超时时间 # 验证SQL(用于连接失效时重试) spring.datasource.druid.validation-query=SELECT 1 spring.datasource.druid.connection-error-retry-attempts=3 # 连接错误重试次数 spring.datasource.druid.break-after-acquire-failure=true ``` > **注意**:此配置仅处理**连接获取阶段**的超时重试,不覆盖 SQL 执行中的超时[^3]。 #### 方案 2:MyBatis-Plus 拦截器实现 SQL 重试(推荐) ```java @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class SqlRetryInterceptor implements Interceptor { private static final int MAX_RETRIES = 3; private static final long RETRY_DELAY = 1000; // 重试间隔(ms) @Override public Object intercept(Invocation invocation) throws Throwable { int retryCount = 0; Throwable lastException; do { try { return invocation.proceed(); } catch (SQLTimeoutException e) { lastException = e; retryCount++; Thread.sleep(RETRY_DELAY); // 延迟后重试 } } while (retryCount < MAX_RETRIES); throw new DataAccessException("SQL重试失败", lastException); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } } ``` **注册拦截器**: ```java @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new SqlRetryInterceptor()); return interceptor; } } ``` #### 方案 3:Spring Retry 声明式重试 1. 添加依赖: ```xml <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> ``` 2. 启用重试机制: ```java @EnableRetry @SpringBootApplication public class Application { ... } ``` 3. 在 Service 层添加重试注解: ```java @Retryable( value = {SQLTimeoutException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000) ) public List<User> queryUsers() { return userMapper.selectList(null); // MyBatis-Plus查询 } ``` #### 关键配置说明 | 配置项 | 说明 | 推荐值 | |--------|------|--------| | `query-timeout` | SQL执行超时阈值 | 2000ms | | `connection-error-retry-attempts` | 连接错误重试次数 | 3 | | `validation-query` | 连接有效性检查SQL | `SELECT 1` | | `max-wait` | 获取连接最大等待时间 | 3000ms | > **最佳实践**: > 1. 生产环境需结合 Druid 监控分析超时原因[^1] > 2. 事务操作慎用重试(需确保操作幂等性) > 3. 重试次数不宜过多(通常 2-3 次) > 4. 超时阈值设置需大于平均 SQL 执行时间 --- ### 相关问题 1. **如何通过 Druid 监控识别慢 SQL?** > 启用 Druid 的 StatFilter 后,可在控制台查看执行时间超过 `slow-sql-millis` 阈值的 SQL[^1] 2. **MyBatis-Plus 如何配置全局 SQL 超时时间?** > 可通过 MyBatis 配置文件设置 `defaultStatementTimeout` 参数,或在 Mapper 方法添加 `@Options(timeout = xxx)` 注解 3. **SQL 重试可能导致哪些副作用?如何避免?** > 可能引发数据重复提交、事务不一致等问题。需确保:① 只对查询操作重试 ② 写操作需实现幂等性 ③ 限制重试次数 4. **如何分连接超时执行超时?** > 连接超时抛出 `ConnectionTimeoutException`,执行超时抛出 `SQLTimeoutException`。Druid 的 `connectTimeout` 和 `queryTimeout` 参数可分别配置[^3] 5. **除超时外,哪些 SQL 异常适合重试?** > 临时性网络错误(如 `CommunicationsException`)、数据库死锁(`SQLTransientException`)等可恢复异常 --- [^1]: 通过 Druid 的 SQL 监控功能可识别慢查询,为超时阈值设置提供依据 [^2]: 数据源基础配置是超时管理的先决条件 [^3]: 连接池参数如 `max-wait` 和 `query-timeout` 直接影响超时行为
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值