Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队

京东云开发者
• 阅读 365

物流的分拣业务在某些分拣场地只有一个数据源,因为数据量比较大,将所有数据存在一张表内查询速度慢,也为了做不同设备数据的分库管理,便在这个数据源内创建了多个不同库名但表完全相同的数据库,如下图所示:

Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队

现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现在的问题来了,该如何满足在配置一个数据源的情况下来查询该数据源下不同数据库的数据呢,借助搜索引擎查到的分库实现大多是借助 Sharding-JDBC 框架,配置多个数据源根据分库算法实现数据源的切换,但是对于只有一个数据源的系统来说,我觉得引入框架再将单个数据源根据不同的库名配置成多个不同的数据源来实现分库查询的逻辑我觉得并不好。

如果我们能在 SQL 执行前将 SQL 中所有的表名前拼接上对应的库名的话,那么就能够实现数据源的切换了,下面我们讲一下使用JSqlParser和 Mybatis拦截器 实现该逻辑,借助 JSqlParser 主要是为了解析SQL,找到其中所有的表名进行拼接,如果大家有更好的实现方式,该组件并不是必须的。

实现逻辑

SqlSource是读取 XML 中 SQL 内容并将其发送给数据库执行的对象,如果我们在执行前能拦截到该对象,并将其中的 SQL 替换掉便达成了我们的目的。SqlSource有多种实现,包括常见的DynamicSqlSource。其中包含着必要的执行逻辑,我们需要做的工作便是在这些逻辑执行完之后,对 SQL 进行改造,所以这次实现我们使用了装饰器模式,在原来的SqlSource上套一层,执行完SqlSource本身的方法之后对其进行增强,代码如下:

public abstract class AbstractDBNameInterceptor {

    /**
     * SqlSource 的装饰器,作用是增强了 getBoundSql 方法,在基础上增加了动态分库的逻辑
     */
    static class SqlSourceDecorator implements SqlSource {

        /**
         * SQL 字段名称
         */
        private static final String SQL_FIELD_NAME = "sql";

        /**
         * 原本的 sql source
         */
        private final SqlSource sqlSource;

        /**
         * 装饰器进行封装
         */
        public SqlSourceDecorator(SqlSource sqlSource) {
            this.sqlSource = sqlSource;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            try {
                // 先生成出未修改前的 SQL
                BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
                // 获取数据库名
                String dbName = getSpecificDBName(parameterObject);
                // 有效才修改
                if (isValid(dbName)) {
                    // 生成需要修改完库名的 SQL
                    String targetSQL = getRequiredSqlWithSpecificDBName(boundSql, dbName);
                    // 更新 SQL
                    updateSql(boundSql, targetSQL);
                }

                return boundSql;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 校验是否为有效库名
         */
        private boolean isValid(String dbName) {
            return StringUtils.isNotEmpty(dbName) && !"null".equals(dbName);
        }

        /**
         * 获取到我们想要的库名的 SQL
         */
        private String getRequiredSqlWithSpecificDBName(BoundSql boundSql, String dbName) throws JSQLParserException {
            String originSql = boundSql.getSql();
            // 获取所有的表名
            Set<String> tables = TablesNamesFinder.findTables(originSql);
            for (String table : tables) {
                originSql = originSql.replaceAll(table, dbName + "." + table);
            }
            return originSql;
        }

        /**
         * 修改 SQL
         */
        private void updateSql(BoundSql boundSql, String sql) throws NoSuchFieldException, IllegalAccessException {
            // 通过反射修改sql语句
            Field field = boundSql.getClass().getDeclaredField(SQL_FIELD_NAME);
            field.setAccessible(true);
            field.set(boundSql, sql);
        }
    }

    // ... 
}

定义了AbstractDBNameInterceptor抽象类是为了实现复用,并将SqlSourceDecorator装饰器定义为静态内部类,这样的话,将所有逻辑都封装在抽象类内部,之后这部分实现好后研发直接实现抽象类的通用方法即可,不必关注它的内部实现。

结合注释我们解释一下SqlSourceDecorator的逻辑,其中用到了 Java 反射相关的操作。首先通过反射获取到 SQL,getSpecificDBName方法是需要自定义实现的,其中parameterObject对象是传到 DAO 层执行查询时的参数,在我们的业务中是能够根据其中的设备相关参数拿到对应的所在库名的,而设备和具体库名的映射关系需要提前初始化好。在获取到具体的库名后执行getRequiredSqlWithSpecificDBName方法来将其拼接到表名前,在这里我们使用到了 JSqlParser 的工具类,解析出来所有的表名,执行字符串的替换,最后一步同样是使用反射操作将该参数值再写回去,这样便完成了指定库名的任务。

接下来我们需要看下抽象拦截器中供拦截器复用的方法,如下:

public abstract class AbstractDBNameInterceptor {

    /**
     * SqlSource 字段名称
     */
    private static final String SQL_SOURCE_FIELD_NAME = "sqlSource";

    /**
     * 执行修改数据库名的逻辑
     */
    protected Object updateDBName(Invocation invocation) throws Throwable {
        // 装饰器装饰 SqlSource
        decorateSqlSource((MappedStatement) invocation.getArgs()[0]);
        return invocation.proceed();
    }

    /**
     * 装饰 SqlSource
     */
    private void decorateSqlSource(MappedStatement statement) throws NoSuchFieldException, IllegalAccessException {
        if (!(statement.getSqlSource() instanceof SqlSourceDecorator)) {
            Field sqlSource = statement.getClass().getDeclaredField(SQL_SOURCE_FIELD_NAME);
            sqlSource.setAccessible(true);
            sqlSource.set(statement, new SqlSourceDecorator(statement.getSqlSource()));
        }
    }
}

这个还是比较简单的,只是借助反射机制做了一层“装饰”,查询拦截器实现如下:

@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})
})
public class SelectDBNameInterceptor extends AbstractDBNameInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return updateDBName(invocation);
    }
}

将其配置到 Mybatis 拦截器中,便能实现数据库动态切换了。

作者:京东物流 王奕龙

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

点赞
收藏
评论区
推荐文章
Easter79 Easter79
3年前
sql优化之大数据量分页查询(mysql)
当需要从数据库查询的表有上万条记录的时候,一次性查询所有结果会变得很慢,特别是随着数据量的增加特别明显,这时就需要使用分页查询。对于数据库分页查询,也有很多种方法和优化的点。谈优化前的准备工作为了对下面列举的一些优化进行测试,需要使用已有的一张表作为实际例子。表名:order\_history。描述:某个业务的订单历史表。主要字段
Easter79 Easter79
3年前
sql注入
反引号是个比较特别的字符,下面记录下怎么利用0x00SQL注入反引号可利用在分隔符及注释作用,不过使用范围只于表名、数据库名、字段名、起别名这些场景,下面具体说下1)表名payload:select\from\users\whereuser\_id1limit0,1;!(https://o
Wesley13 Wesley13
3年前
java 微服务分布式 springcloud vue.js flowable 流程引擎
1.代码生成器:\正反双向\(单表、主表、明细表、树形表,快速开发利器)freemaker模版技术,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本、处理类、service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池dru
Easter79 Easter79
3年前
springcloud vue 微服务分布式 activiti工作流 前后分离 集成代码生成器 shiro权限
1.代码生成器:\正反双向\(单表、主表、明细表、树形表,快速开发利器)freemaker模版技术,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本、处理类、service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池dru
皕杰报表(关于日期时间时分秒显示不出来)
在使用皕杰报表设计器时,数据据里面是日期型,但当你web预览时候,发现有日期时间类型的数据时分秒显示不出来,只有年月日能显示出来,时分秒显示为0:00:00。1.可以使用tochar解决,数据集用selecttochar(flowdate,"yyyyMMddHH:mm:ss")fromtablename2.也可以把数据库日期类型date改成timestamp
皕杰报表的初使用
使用皕杰报表,首先得配置数据库驱动,在首选项里面添加。然后的配置数据源,新建数据源,输入数据源名称,下一步,选择数据源类型,选择数据源的驱动程序,编写url及用户、密码等。单击“检测数据源”按钮,出现“连接成功”表示数据库连接成功。单击“完成”即可,如果不能正确配置,都会无法连接数据库。下来新建报表,如果是想简单的查询,可以新建展现报表,如果需要往数据库插
Wesley13 Wesley13
3年前
SSH实现动态数据源切换,事务场景下使用AOP
上周写代码遇到了切换数据源的问题,在同一个方法中向两个不同数据源做一些操作,但是这个方法使用了事务,所以网上一般动态切换数据源的方法就失效了。框架是spirngmvchibernate,数据库是oracle,连接池druid。一般情况下,操作数据都是在DAO层进行处理。一种办法是使用多个DataSource然后创建多个SessionFa
皕杰报表之填报操作·
“新建智能映射”:新建填报操作填报操作名称:设置这个填报操作的名称数据源:设置这个填报操作映射的数据源点击“下一步”,选择数据库表Schema:选择这个数据库的Schema。表名筛选:查找需要选择的数据表,不区分大小写。勾选需要填报的数据表,点击“下一步”,设置填报字段填报单元格:选择填报操作时,这个字段对应报表中的哪个字段的值。勾选主键:当数据进行填报时
Wesley13 Wesley13
3年前
MySQL 8.0窗口函数优化SQL一例
8.0的窗口函数真香1\.问题描述最近在折腾把所有mysqlslowquerylog写入到数据库中,再集中展示,向业务部门开放,也方便业务部门的同学自行查看并优化各自业务内的慢SQL。增加了定期生成报表的功能,统计最近1~2周内的慢查询数量变化情况,给业务方同学更直观的数据对比,了解最近这段时间的慢查询数量变化情况,是多
Wesley13 Wesley13
3年前
mysql可扩展第二部分
  数据分片主要是将数据按照一定的规则分为几个完全不同的数据集合的方式成为数据分片。数据的切分可以是数据库内的,将数据库中的一张表切分为几个不同的数据库表。也可以是数据库级别的,将数据库中的表划分为多个表,这些表存储在不同的数据库服务器上。该部分主要用来介绍数据库级的数据分片。切分规则将具有相关的数据保存在同一个分片上可以提高数据查询效率。数据库分片的路由规