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

现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现在的问题来了,该如何 满足在配置一个数据源的情况下来查询该数据源下不同数据库的数据 呢,借助搜索引擎查到的分库实现大多是借助 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 转载请注明来源

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

  1. 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离

    前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...

  2. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  3. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  4. mybatis拦截器 修改mybatis返回结果集中的字段的值

    项目中使用了shardingJDBC,业务库做了分库,公共库没在一起,所以导致做码值转换的时候,需要在实现类里面做转码,重复的代码量大,故考虑用mybatis拦截器,将码值转换后再做返回给实现类.   ...

  5. 解决mybatis拦截器无法注入spring bean的问题

    公司要整合rabbitmq与mybatis拦截器做一个数据同步功能. 整合过程中大部分环节都没什么问题,就是遇到了mybatis拦截器 @Intercepts(@Signature(type = Ex ...

  6. Mybatis拦截器

    Mybatis拦截器

  7. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  8. MyBatis拦截器原理探究

    MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...

  9. Mybatis拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...

  10. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

随机推荐

  1. 后CNN探索,如何用RNN进行图像分类

    摘要:RNN可以用于描述时间上连续状态的输出,有记忆功能,能处理时间序列的能力,让我惊叹. 本文分享自华为云社区<用RNN进行图像分类--CNN之后的探索>,作者: Yin-Manny. ...

  2. LiteOS内核源码分析:消息队列Queue

    摘要:本文通过分析LiteOS队列模块的源码,掌握队列使用上的差异. 队列(Queue)是一种常用于任务间通信的数据结构.任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务:当队列中有新消 ...

  3. 云图说|初识云数据库GaussDB(for Cassandra)

    摘要:不用再为数据不一致苦恼,因为强一致的Cassandra来了,DBA们不用加班修数据了.GaussDB(for Cassandra)是一款基于华为自主研发的计算存储分离架构的分布式云数据库服务,是 ...

  4. Solon2 开发之IoC,二、构建一个 Bean 的三种方式

    1.手动 简单的构建: //生成普通的Bean Solon.context().wrapAndPut(UserService.class, new UserServiceImpl()); //生成带注 ...

  5. AI 0基础学习,数学名词解析

    AI学习过程中,常见的名词解析 中位数 将数据从小到大排序,奇数列,取中间值,偶数列,中间两个值的平均,可做为销售指标 众数 一组数据中,数值出现最多的那个.反映哪款产品,销量最好 平均数 比赛中,去 ...

  6. Nginx The system cannot find the path specified

    Nginx -t 时报  (3: The system cannot find the path specified) 原因:路径中有文件夹是中文,Nginx 全路径,不能包含中文

  7. CentOS 7 qemu 创建虚拟机简单流程

    查看是否支持虚拟化 egrep -o '(vmx|svm)' /proc/cpuinfo 如果有显示则支持虚拟化,没有显示则不支持   安装qemu 和virt 系列软件包 yum install q ...

  8. 比 Python 快得吓人,PyPy 极简入门

    众所周知 Python 有一个致命的缺点:速度比 C.C ++ 等语言慢很多.PyPy 恰好可以解决这一问题,它能够让 Python 代码运行得比 C 还快. 比如: import time from ...

  9. Java 剑指offer(16) 打印1到最大的n位数

    题目 输入数字n,按顺序打印出从1最大的n位十进制数.比如输入3,则打印出1.2.3一直到最大的3位数即999. 思路 陷阱: n过大时是大数问题,不能简单用int或者long数据输出,需要采用字符串 ...

  10. 以API 配置的方式来配置你的 Dubbo 应用

    package com.aswatson.csc.member.conf; import com.aswatson.csc.member.service.MemberCardService; impo ...