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 转载请注明来源
Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队的更多相关文章
- 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离
前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...
- 基于Spring和Mybatis拦截器实现数据库操作读写分离
首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...
- 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表
利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...
- mybatis拦截器 修改mybatis返回结果集中的字段的值
项目中使用了shardingJDBC,业务库做了分库,公共库没在一起,所以导致做码值转换的时候,需要在实现类里面做转码,重复的代码量大,故考虑用mybatis拦截器,将码值转换后再做返回给实现类. ...
- 解决mybatis拦截器无法注入spring bean的问题
公司要整合rabbitmq与mybatis拦截器做一个数据同步功能. 整合过程中大部分环节都没什么问题,就是遇到了mybatis拦截器 @Intercepts(@Signature(type = Ex ...
- Mybatis拦截器
Mybatis拦截器
- Mybatis拦截器 mysql load data local 内存流处理
Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...
- MyBatis拦截器原理探究
MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...
- Mybatis拦截器介绍
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...
- Mybatis拦截器实现分页
本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...
随机推荐
- web messaging与Woker分类:漫谈postMessage跨线程跨页面通信
web messaging 跨文档通信(cross-document messaging):跨就是我们国内更为熟知的HTML5 window.postMessage()应用的那种通信: 通道通信(ch ...
- 提升数据决策时效,火山引擎DataLeapCDC分库分表能力升级!
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 近日,大数据研发治理套件DataLeap数据集成更新CDC分库分表能力,可做到将多个实例的多个数据库的多个分表 ...
- 火山引擎在行为分析场景下的ClickHouse JOIN优化
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 背景 火山引擎增长分析DataFinder基于ClickHouse来进行行为日志的分析,ClickHouse的主要 ...
- SpringBoot 自定义初始化任务 Runner
在项目启动的时候需要做一些初始化的操作,比如初始化线程池,提前加载好加密证书等.可以通过实现Runner接口完成以上工作. 两者只是参数上的区别 方式一 实现 CommandLineRunner 接口 ...
- 收到一封CTO来信,邀约面试机器学习工程师
大家好,我是北海 很少登陆 Gmail,前天收验证码登了一下,发现居然收到一封某初创公司CTO的来信. 我在Github上看到了您的资料觉得很有意思,请问您是否考虑我们公司的全职工作机会呢?可供考虑的 ...
- 一文聊透 IP 地址的那些事
IP 地址,是一个大家都耳熟能详的名词.以生活举例,IP 在互联网中的作用就像是寄件时的收件人地址和寄件人地址,收件人地址让信件可以被正确送达,寄件人地址则让收到信的人可以回信. IP 地址作为每一个 ...
- JS 实现 HashMap
HashMap代码(这种实现方式是错误的,错误原因:代码中_map._length变量是HashMap的所有实例共用的): /** * HashMap * 2021年09月09日 */ (functi ...
- C++右值引用与转移语义简要介绍
在 C++11 之前,值类型变量的传递会导致把它完整的拷贝一份 比如说把一个 vector 作为函数返回值赋值给某个局部变量,他就会调用 vector 的拷贝构造函数创建一个完整的副本,把这个副本作为 ...
- 【第三方库】从编译到运行,轻松学会gflags库
gflags是Google开源的一个库,可以很方便地定义一些全局变量,并且可以从命令行设置他们的值,广泛应用于各个项目中以及自己平时的开发中.本期参考gflags的官方文档,简单直接介绍下怎么使用这个 ...
- BTC-协议
BTC-协议 一个去中心化的数字货币要解决两个问题 1.谁有权发行货币 比特币的发行是由挖矿决定的(coinbase transaction 唯一一个产生新币的途径)比特币通过挖矿来决定货币的发行权, ...