背景

项目中使用了读写分离的数据库访问框架,这个框架基于 Proxy 模式,对使用方屏蔽了主从数据库,查询时默认路由到从库。但是因为存在主从延迟,当主库数据变更较大时延迟会达到秒级,造成了一些线上问题。中间件同事建议在对主从延迟敏感的场景中绑定主库查询,绑定方式是在 SQL 语句的开头添加特定的注释字符串,访问框架会根据这个字符串执行主库路由。这个场景涉及到 SQL 语句的拦截修改,立即想到了如下两个方案,评估后最终选择了第二个方案

  1. 基于 MyBatis 的 @Intercepts 拦截器实现,这种方式如果要做到方法级别的拦截修改,需要一些方法标注的额外开发量
  2. 扩展 MyBatis-plus 的通用方法,增加专门的走主库的查询方法

1. 自定义通用方法的实现

MyBatis-plus 提供了许多通用的数据库操作方法,其定义包含在枚举com.baomidou.mybatisplus.core.enums.SqlMethod中,仿照其实现,自定义一个通用的数据库操作方法的步骤如下

1.1 新增 Mapper 方法与 SQL 语句脚本映射枚举

新增枚举SqlMethodEx,其内部属性定义与 SqlMethod 完全一致

SqlMethodEx实际维护了上层 Mapper 方法与底层 SQL 语句脚本的映射,本例中 MASTER_SELECT_LIST 定义了一个 Mapper#selectListFromMaster() 方法及其对应的 SQL 语句脚本的基本结构,可以看到本例中笔者在 SQL 语句开始处添加了一个前缀字符串

   public enum SqlMethodEx {

    MASTER_SELECT_LIST("selectListFromMaster", "从主库查询满足条件多条数据",
            "<script>%s " + SqlConstant.DA_MASTER_PARAM + "SELECT %s FROM %s %s %s\n</script>"),
    ;


    private final String method;
    private final String desc;
    private final String sql;

    SqlMethodEx(String method, String desc, String sql) {
        this.method = method;
        this.desc = desc;
        this.sql = sql;
    }

    public String getMethod() {
        return method;
    }

    public String getDesc() {
        return desc;
    }

    public String getSql() {
        return sql;
    }

   }

1.2 新增通用方法的定义类

新增 SelectListFromMasterMethod 类继承于 AbstractMethod,负责将我们自定义的通用 Mapper方法 及其对应的 SQL 语句脚本组装为 MappedStatement 对象并添加到容器中,对 MappedStatement 对象不了解的读者可参考 MyBatis Mapper 操作数据库源码流程总结

   public class SelectListFromMasterMethod extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

        SqlMethodEx sqlMethod = SqlMethodEx.MASTER_SELECT_LIST;
        String sqlSelectColumns = sqlSelectColumns(tableInfo, true);
        String sqlWhere = sqlWhereEntityWrapper(true, tableInfo);
        String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns, tableInfo.getTableName(), sqlWhere, sqlComment());
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
    }

    private String getMethod(SqlMethodEx sqlMethod) {
        return sqlMethod.getMethod();
    }
}

1.3 新增 SQL 注入器

新增 MasterSqlInjector 类继承于 DefaultSqlInjector,负责将所有通用的 Mapper方法 的定义类收集起来,后续将使用这些方法定义类为每一个具体的 Mapper 添加通用方法

public class MasterSqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methods = super.getMethodList(mapperClass);
        methods.add(new SelectListFromMasterMethod());
        return methods;
    }

}

1.4 新增配置类将 SQL 注入器添加到容器

MyBatis-plus 的组件注入和普通的 Spring 配置注入完全一致,本例中笔者自定义的 SQL 注入器 MasterSqlInjector 注入到容器后,即相当于提供了 Mapper#selectListFromMaster() 方法的底层 SQL 语句基础,接下来则需要定义上层的 Mapper 方法供使用方调用

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MasterSqlInjector masterSqlInjector() {
        return new MasterSqlInjector();
    }
}

1.5 新增基类 Mapper

新增 MasterMapper 继承于 BaseMapper,并在其中定义一个 selectListFromMaster方法

使用时具体的业务 Mapper 只需要继承 MasterMapper 即可像使用其它MyBatis-plus 提供的全局方法一样使用其定义的 MasterMapper#selectListFromMaster() 方法

public interface MasterMapper<T> extends BaseMapper<T> {
    /**
     * 根据 entity 条件,从主库查询任意条记录
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    List<T> selectListFromMaster(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

}

2. 实现原理

在这里插入图片描述

2.1 自定义 SQL 注入器的注入

MyBatis-plus 源码解析 中,笔者提到了自动配置类配置SqlSessionFactoryMybatisPlusAutoConfiguration#sqlSessionFactory()方法 ,可以看到本文中自定义的 SQL 注入器通过 @Bean 托管给 Spring 后,也是在这里存入到 MyBatis-plusGlobalConfig 全局缓存中的

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        
        ......
        
        // TODO 此处必为非 NULL
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
       ......
        // TODO 注入sql注入器
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
       
        ......
        
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }

    private <T> void getBeanThen(Class<T> clazz, Consumer<T> consumer) {
        if (this.applicationContext.getBeanNamesForType(clazz, false, false).length > 0) {
            consumer.accept(this.applicationContext.getBean(clazz));
        }
    }

2.2 自定义 SQL 注入器的使用

MyBatis-plus 源码解析 分析了 MyBatis-plus 配置工作的主要流程,SQL 注入器的使用也在其中,这一部分本文不再重复。简单来说,就是根据 MyBatis-plus 提供的操作数据库的通用方法给每一个 Mapper 准备对应的 MappedStatement 对象的过程

2.3 Mapper 操作数据库的实现

这部分和本文的相关性比较大,但是内容细节非常多,为了分析的完整,读者可参考 MyBatis Mapper 操作数据库源码流程总结 做大致了解,关于 SQL 语句的细节处理可参考 MyBatis-plus 转化处理 SQL 语句的源码分析

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐